React Router v7 已发布。 查看文档
常见问题

常见问题

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

你不能 😅。在客户端过渡期间,为了使你的应用程序尽可能快速,Remix 将并行调用你所有的加载器,在单独的获取请求中。它们中的每一个都需要有自己的身份验证检查。

这可能与你使用 Remix 之前所做的没有区别,现在可能只是更明显了。在 Remix 之外,当你向你的“API 路由”发出多个获取请求时,每个端点都需要验证用户会话。换句话说,Remix 路由加载器是它们自己的“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 和处理它的服务器代码放置在一起。开发人员经常想知道在这种情况下如何处理多个操作。你有两个选择

  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 此功能,请参阅 Event Submitter PolyfillFormData Submitter Polyfill。有关更多详细信息,请参阅相关问题 remix-run/remix#9704

如何在表单中包含结构化数据?

如果你习惯使用 application/json 内容类型进行获取,你可能会想知道表单如何适应这一点。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