FAQs

常见问题

¥Frequently Asked Questions

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

¥How can I have a parent route loader validate the user and protect all child routes?

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

¥You can't 😅. During a client-side transition, to make your app as speedy as possible, Remix will call all of your loaders in parallel, in separate fetch requests. Each one of them needs to have its own authentication check.

这可能与你在 Remix 之前的做法没什么不同,只是现在可能更明显了。在 Remix 之外,当你多次获取 "API 路由" 数据时,每个端点都需要验证用户会话。换句话说,Remix 路由加载器是它们自己的 "API 路由" 函数,必须如此对待。

¥This is probably not different from what you were doing before Remix, it might just be more obvious now. Outside of Remix, when you make multiple fetches to your "API Routes", each of those endpoints needs to validate the user session. In other words, Remix route loaders are their own "API Route" and must be treated as such.

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

¥We recommend you create a function that validates the user session that can be added to any routes that require it.

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;
}

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

¥And now in any loader or action that requires a user session, you can call the function.

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);
}

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

¥Even if you don't need the session information, the function will still protect the route:

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

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

¥How do I handle multiple forms in one route?

在 YouTube 上观看

¥Watch on YouTube

在 HTML 中,表单可以通过 action 属性提交到任何 URL,应用将导航到该 URL:

¥In HTML, forms can post to any URL with the action prop, and the app will navigate there:

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

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

¥In Remix the action defaults to the route that the form is rendered in, making it easy to co-locate the UI and the server code that handles it. Developers often wonder how you can handle multiple actions in this scenario. You have two choices:

  1. 发送表单字段以确定你想要执行的操作

    ¥Send a form field to determine the action you want to take

  2. 发布到不同的路由并重定向回原始路由

    ¥Post to a different route and redirect back to the original

我们发现选项 (1) 最简单,因为你无需费力处理会话即可将验证错误返回到 UI。

¥We find option (1) to be the simplest because you don't have to mess around with sessions to get validation errors back to the UI.

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

¥HTML buttons can send a value, so it's the easiest way to implement this:

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:提交者属性FormData() 构造函数提交器参数。请务必检查浏览器对这些功能的兼容性。如果你需要对此进行 polyfill,请参阅 事件提交器 Polyfill表单数据提交器 Polyfill。有关更多详细信息,请参阅相关问题 remix-run/remix#9704

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

¥How can I have structured data in a form?

如果你习惯使用 application/json 内容类型进行获取,你可能会想知道表单如何适应这种情况。FormData 与 JSON 略有不同。

¥If you're used to doing fetches with a content type of application/json, you may wonder how forms fit into this. FormData is a bit different from JSON.

  • 它不能包含嵌套数据,它只是 "键值"。

    ¥It can't have nested data, it's just "key value".

  • 与 JSON 不同,它可以在一个键上包含多个条目。

    ¥It can have multiple entries on one key, unlike JSON.

如果你只想将结构化数据发送到 post 数组,则可以在多个输入中使用相同的键:

¥If you're wanting to send structured data simply to post arrays, you can use the same key on multiple inputs:

<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() 访问复选框值。

¥Each checkbox has the name: "category". Since FormData can have multiple values on the same key, you don't need JSON for this. Access the checkbox values with formData.getAll() in your action.

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

使用相同的输入名称和 formData.getAll() 可以满足大多数在表单中提交结构化数据的情况。

¥Using the same input name and formData.getAll() covers most cases for wanting to submit structured data in your forms.

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

¥If you still want to submit nested structures as well, you can use non-standard form-field naming conventions and the query-string package from npm:

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

然后在你的操作中:

¥And then in your action:

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 转储到隐藏字段中。请注意,此方法不适用于渐进式增强。如果这对你的应用不重要,这是一种发送结构化数据的简便方法。

¥Some folks even dump their JSON into a hidden field. Note that this approach won't work with progressive enhancement. If that's not important to your app, this is an easy way to send structured data.

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

然后在操作中解析它:

¥And then parse it in the action:

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

同样,formData.getAll() 通常就足够了,我们鼓励你尝试一下!

¥Again, formData.getAll() is often all you need, we encourage you to give it a try!

Remix v2.17 中文网 - 粤ICP备13048890号