React Router v7 已發佈。 查看文件
常見問題

常見問題

如何讓父路由載入器驗證使用者並保護所有子路由?

你無法這樣做 😅。在客戶端轉換期間,為了讓你的應用程式盡可能快速,Remix 會並行呼叫你的所有載入器,在獨立的 fetch 請求中進行。它們每一個都需要有自己的身份驗證檢查。

這可能與你使用 Remix 之前所做的沒有什麼不同,現在可能只是更明顯了。在 Remix 之外,當你對你的「API 路由」進行多次 fetch 時,每個端點都需要驗證使用者會話。換句話說,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 中,動作預設為呈現表單的路由,使其易於共同放置 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 的內容類型執行 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" />
</>

然後在您的 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 傾印到一個隱藏的欄位中。請注意,這種方法不適用於漸進式增強。如果這對您的應用程式來說不重要,這是一種發送結構化資料的簡單方法。

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

然後在 action 中解析它

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

再次強調,formData.getAll() 通常已足夠您使用,我們鼓勵您嘗試看看!

文件和範例以 MIT 授權