React Router v7 已發布。 查看文件
伺服器與客戶端程式碼執行

伺服器 vs. 客戶端程式碼執行

Remix 會在伺服器以及瀏覽器上執行您的應用程式。然而,它並非在兩處都執行您的所有程式碼。

在建置步驟中,編譯器會建立伺服器建置和客戶端建置。伺服器建置會將所有內容打包成單一模組(或在使用伺服器綁定時打包成多個模組),但客戶端建置會將您的應用程式分成多個綁定,以優化瀏覽器中的載入。它也會從綁定中移除伺服器程式碼。

以下路由導出以及其中使用的依賴項會從客戶端建置中移除

考慮上一節中的這個路由模組

import type {
  ActionFunctionArgs,
  HeadersFunction,
  LoaderFunctionArgs,
} from "@remix-run/node"; // or cloudflare/deno
import { json } from "@remix-run/node"; // or cloudflare/deno
import { useLoaderData } from "@remix-run/react";

import { getUser, updateUser } from "../user";

export const headers: HeadersFunction = () => ({
  "Cache-Control": "max-age=300, s-maxage=3600",
});

export async function loader({
  request,
}: LoaderFunctionArgs) {
  const user = await getUser(request);
  return json({
    displayName: user.displayName,
    email: user.email,
  });
}

export default function Component() {
  const user = useLoaderData<typeof loader>();
  return (
    <Form action="/account">
      <h1>Settings for {user.displayName}</h1>

      <input
        name="displayName"
        defaultValue={user.displayName}
      />
      <input name="email" defaultValue={user.email} />

      <button type="submit">Save</button>
    </Form>
  );
}

export async function action({
  request,
}: ActionFunctionArgs) {
  const formData = await request.formData();
  const user = await getUser(request);

  await updateUser(user.id, {
    email: formData.get("email"),
    displayName: formData.get("displayName"),
  });

  return json({ ok: true });
}

伺服器建置會將整個模組包含在最終綁定中。然而,客戶端建置會移除 actionheadersloader,以及依賴項,產生如下結果

import { useLoaderData } from "@remix-run/react";

export default function Component() {
  const user = useLoaderData();
  return (
    <Form action="/account">
      <h1>Settings for {user.displayName}</h1>

      <input
        name="displayName"
        defaultValue={user.displayName}
      />
      <input name="email" defaultValue={user.email} />

      <button type="submit">Save</button>
    </Form>
  );
}

分割客戶端和伺服器程式碼

Vite 開箱即用不支援在同一個模組中混合僅限伺服器的程式碼和客戶端安全程式碼。Remix 能夠對路由做出例外,因為我們知道哪些導出僅限伺服器,並且可以從客戶端中移除它們。

在 Remix 中隔離僅限伺服器程式碼有幾種方法。最簡單的方法是使用 .server.client 模組。

.server 模組

雖然不是絕對必要,但.server 模組是明確將整個模組標記為僅限伺服器的好方法。如果 .server 檔案或 .server 目錄中的任何程式碼意外進入客戶端模組圖,則建置會失敗。

app
├── .server 👈 marks all files in this directory as server-only
│   ├── auth.ts
│   └── db.ts
├── cms.server.ts 👈 marks this file as server-only
├── root.tsx
└── routes
    └── _index.tsx

.server 模組必須位於您的 Remix 應用程式目錄中。

僅在使用 Remix Vite 時才支援 .server 目錄。Classic Remix Compiler 僅支援 .server 檔案。

.client 模組

您可能會依賴即使在伺服器上綁定也不安全的客戶端程式庫 — 也許它只是因為被引入而嘗試存取 window

您可以透過在檔案名稱後附加 *.client.ts 或將它們巢狀在 .client 目錄中,從伺服器建置中移除這些模組的內容。

僅在使用 Remix Vite 時才支援 .client 目錄。Classic Remix Compiler 僅支援 .client 檔案。

vite-env-only

如果您想在同一個模組中混合僅限伺服器程式碼和客戶端安全程式碼,您可以使用vite-env-only。這個 Vite 外掛程式允許您明確地將任何表達式標記為僅限伺服器,以便它在客戶端中被替換為 undefined

例如,一旦您將外掛程式新增至您的 Vite 設定,您就可以使用 serverOnly$ 包裝任何僅限伺服器的導出

import { serverOnly$ } from "vite-env-only";

import { db } from "~/.server/db";

export const getPosts = serverOnly$(async () => {
  return db.posts.findMany();
});

export const PostPreview = ({ title, description }) => {
  return (
    <article>
      <h2>{title}</h2>
      <p>{description}</p>
    </article>
  );
};

這個範例會為客戶端編譯成以下程式碼

export const getPosts = undefined;

export const PostPreview = ({ title, description }) => {
  return (
    <article>
      <h2>{title}</h2>
      <p>{description}</p>
    </article>
  );
};
文件和範例以 MIT