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 目录。经典 Remix 编译器仅支持 .server 文件。

.client 模块

您可能依赖于客户端库,这些库甚至在服务器上捆绑也是不安全的——也许它只是通过导入来尝试访问window

您可以通过将 *.client.ts 附加到文件名或将它们嵌套在 .client 目录中,从而从服务器构建中删除这些模块的内容。

仅在使用Remix Vite时才支持 .client 目录。经典 Remix 编译器仅支持 .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