常见问题

常见问题

如何让父路由加载器验证用户并保护所有子路由?

你做不到😅。在客户端过渡期间,为了使你的应用尽可能快速,Remix 会并行调用所有加载器,并在单独的 fetch 请求中进行。每个加载器都需要进行自己的身份验证检查。

这可能与你在 Remix 之前所做的事情没有区别,只是现在可能更明显了。在 Remix 之外,当你向你的“API 路由”发出多个 fetch 请求时,每个端点都需要验证用户会话。换句话说,Remix 路由加载器本身就是“API 路由”,必须像对待 API 路由一样对待它们。

我们建议你创建一个验证用户会话的函数,可以将其添加到任何需要它的路由中。

import {
  createCookieSessionStorage,
  redirect,
} from "@remix-run/node"; // or cloudflare/deno

// somewhere you've got a session storage
const { getSession } = createCookieSessionStorage();

export async function requireUserSession(request) {
  // get the session
  const cookie = request.headers.get("cookie");
  const session = await getSession(cookie);

  // validate the session, `userId` is just an example, use whatever value you
  // put in the session when the user authenticated
  if (!session.has("userId")) {
    // if there is no user session, redirect to login
    throw redirect("/login");
  }

  return session;
}

现在,在任何需要用户会话的加载器或操作中,你都可以调用该函数。

export async function loader({
  request,
}: LoaderFunctionArgs) {
  // if the user isn't authenticated, this will redirect to login
  const session = await requireUserSession(request);

  // otherwise the code continues to execute
  const projects = await fakeDb.projects.scan({
    userId: session.get("userId"),
  });
  return json(projects);
}

即使你不需要会话信息,该函数仍然会保护路由。

export async function loader({
  request,
}: LoaderFunctionArgs) {
  await requireUserSession(request);
  // continue
}

如何在一条路由中处理多个表单?

在 YouTube 上观看

在 HTML 中,表单可以通过 action 属性发送到任何 URL,应用会导航到那里。

<Form action="/some/where" />

在 Remix 中,action 默认设置为渲染表单的路由,这使得 UI 和处理它的服务器代码可以轻松地放在一起。开发人员常常想知道在这种情况下如何处理多个 action。你有两种选择。

  1. 发送一个表单字段来确定你想要执行的操作。
  2. 发送到另一个路由,然后重定向回原始路由。

我们发现选项 (1) 最简单,因为你无需使用会话将验证错误返回到 UI。

HTML 按钮可以发送值,因此这是实现此功能最简单的方法。

export async function action({
  request,
}: ActionFunctionArgs) {
  const formData = await request.formData();
  const intent = formData.get("intent");
  switch (intent) {
    case "update": {
      // do your update
      return updateProjectName(formData.get("name"));
    }
    case "delete": {
      // do your delete
      return deleteStuff(formData);
    }
    default: {
      throw new Error("Unexpected action");
    }
  }
}

export default function Projects() {
  const project = useLoaderData<typeof loader>();
  return (
    <>
      <h2>Update Project</h2>
      <Form method="post">
        <label>
          Project name:{" "}
          <input
            type="text"
            name="name"
            defaultValue={project.name}
          />
        </label>
        <button type="submit" name="intent" value="update">
          Update
        </button>
      </Form>

      <Form method="post">
        <button type="submit" name="intent" value="delete">
          Delete
        </button>
      </Form>
    </>
  );
}

较旧的浏览器版本可能会破坏此功能,因为它们可能不支持SubmitEvent: submitter 属性FormData() 构造函数的 submitter 参数。请务必检查这些功能的浏览器兼容性。如果你需要进行 polyfill,请参阅事件提交 PolyfillFormData 提交 Polyfill。有关更多详细信息,请参阅相关问题remix-run/remix#9704

如何在表单中使用结构化数据?

如果你习惯于使用内容类型为application/json的 fetch,你可能会想知道表单是如何融入其中的。FormData与 JSON 有些不同。

  • 它不能包含嵌套数据,它只是“键值对”。
  • 可以在一个键上包含多个条目,这与 JSON 不同。

如果你想发送结构化数据只是为了发布数组,你可以对多个输入使用相同的键。

<Form method="post">
  <p>Select the categories for this video:</p>
  <label>
    <input type="checkbox" name="category" value="comedy" />{" "}
    Comedy
  </label>
  <label>
    <input type="checkbox" name="category" value="music" />{" "}
    Music
  </label>
  <label>
    <input type="checkbox" name="category" value="howto" />{" "}
    How-To
  </label>
</Form>

每个复选框的名称都是:“category”。由于FormData可以在同一个键上有多个值,因此你不需要为此使用 JSON。在你的操作中使用formData.getAll()访问复选框的值。

export async function action({
  request,
}: ActionFunctionArgs) {
  const formData = await request.formData();
  const categories = formData.getAll("category");
  // ["comedy", "music"]
}

使用相同的输入名称和formData.getAll()涵盖了大多数想要在表单中提交结构化数据的用例。

如果你仍然想要提交嵌套结构,你可以使用非标准的表单字段命名约定和来自 npm 的query-string包。

<>
  // arrays with []
  <input name="category[]" value="comedy" />
  <input name="category[]" value="comedy" />
  // nested structures parentKey[childKey]
  <input name="user[name]" value="Ryan" />
</>

然后在你的操作中

import queryString from "query-string";

// in your action:
export async function action({
  request,
}: ActionFunctionArgs) {
  // use `request.text()`, not `request.formData` to get the form data as a url
  // encoded form query string
  const formQueryString = await request.text();

  // parse it into an object
  const obj = queryString.parse(formQueryString);
}

有些人甚至将他们的 JSON 放入一个隐藏字段中。请注意,这种方法不适用于渐进增强。如果这对你的应用不重要,这是一种发送结构化数据的简单方法。

<input
  type="hidden"
  name="json"
  value={JSON.stringify(obj)}
/>

然后在操作中解析它。

export async function action({
  request,
}: ActionFunctionArgs) {
  const formData = await request.formData();
  const obj = JSON.parse(formData.get("json"));
}

同样,formData.getAll()通常是你所需要的,我们鼓励你尝试一下!

文档和示例根据以下许可证授权 MIT