常见问题
本页

注意事项

使用 React 在服务器和浏览器中渲染您的应用程序存在一些固有的注意事项。此外,在构建 Remix 时,我们一直专注于生产结果和可扩展性。一些开发人员体验和生态系统兼容性问题尚未解决。

本文档将帮助您克服这些障碍。

typeof window 检查

由于相同的 JavaScript 代码可以在浏览器和服务器中运行,因此有时您需要在代码的一部分中仅在其中一个上下文中运行。

if (typeof window === "undefined") {
  // running in a server environment
} else {
  // running in a browser environment
}

这在 Node.js 环境中可以正常工作,但是 Deno 实际上支持 window!因此,如果您确实要检查是否在浏览器中运行,最好检查 document

if (typeof document === "undefined") {
  // running in a server environment
} else {
  // running in a browser environment
}

这将适用于所有 JS 环境(Node.js、Deno、Workers 等)。

浏览器扩展注入代码

您可能会在浏览器中遇到此警告。

Warning: Did not expect server HTML to contain a <script> in <html>.

这是来自 React 的水合警告,很可能是由于您的某个浏览器扩展将脚本注入到服务器渲染的 HTML 中,从而导致生成的 HTML 存在差异。

请以隐身模式查看页面,警告应该消失。

loader 中写入会话

通常,您应该只在操作中写入会话,但有时在加载程序中这样做是有意义的(匿名用户、导航跟踪等)。

虽然多个加载程序可以从同一个会话中读取,但在加载程序中写入会话会导致问题。

Remix 加载程序并行运行,有时在单独的请求中(客户端转换调用 fetch 以获取每个加载程序)。如果一个加载程序正在写入会话,而另一个加载程序尝试从中读取,则会导致错误和/或非确定性行为。

此外,会话是建立在来自浏览器的请求的 cookie 之上的。在提交会话后,它会通过 Set-Cookie 标头发送到浏览器,然后在下一个请求中通过 Cookie 标头发送回服务器。无论并行加载程序如何,您都不能使用 Set-Cookie 写入 cookie,然后尝试从原始请求的 Cookie 中读取它并期望获得更新的值。它需要先进行往返到浏览器,然后才能从下一个请求中获取。

如果您需要在加载程序中写入会话,请确保加载程序不与任何其他加载程序共享该会话。

客户端包中的服务器代码

本节内容仅适用于使用Classic Remix Compiler的情况。

您可能会在浏览器中遇到这种奇怪的错误。几乎总是意味着服务器代码进入了浏览器包。

TypeError: Cannot read properties of undefined (reading 'root')

例如,您不能直接将fs-extra导入路由模块。

import { json } from "@remix-run/node"; // or cloudflare/deno
import fs from "fs-extra";

export async function loader() {
  return json(await fs.pathExists("../some/path"));
}

export default function SomeRoute() {
  // ...
}

要解决此问题,请将导入移动到名为*.server.ts*.server.js的不同模块,然后从那里导入。在本例中,我们将在utils/fs-extra.server.ts创建一个新文件。

export { default } from "fs-extra";

然后将路由中的导入更改为新的“包装器”模块。

import { json } from "@remix-run/node"; // or cloudflare/deno

import fs from "~/utils/fs-extra.server";

export async function loader() {
  return json(await fs.pathExists("../some/path"));
}

export default function SomeRoute() {
  // ...
}

更妙的是,向项目发送一个PR,将"sideEffects": false添加到他们的package.json,以便进行树摇的打包器可以安全地从浏览器包中删除代码。

同样,如果您在路由模块的顶层范围内调用依赖于仅服务器代码的函数,您也可能会遇到相同的错误。

例如,Remix 上传处理程序,如unstable_createFileUploadHandlerunstable_createMemoryUploadHandler在幕后使用 Node 全局变量,并且应仅在服务器上调用。您可以在*.server.ts*.server.js文件中调用这两个函数中的任何一个,或者您可以将它们移至路由的actionloader函数。

因此,与其这样做:

import { unstable_createFileUploadHandler } from "@remix-run/node"; // or cloudflare/deno

const uploadHandler = unstable_createFileUploadHandler({
  maxPartSize: 5_000_000,
  file: ({ filename }) => filename,
});

export async function action() {
  // use `uploadHandler` here ...
}

您应该这样做:

import { unstable_createFileUploadHandler } from "@remix-run/node"; // or cloudflare/deno

export async function action() {
  const uploadHandler = unstable_createFileUploadHandler({
    maxPartSize: 5_000_000,
    file: ({ filename }) => filename,
  });

  // use `uploadHandler` here ...
}

为什么会出现这种情况?

Remix 使用“树摇”从浏览器包中删除服务器代码。路由模块actionheadersloader导出中的任何内容都将被删除。这是一个很棒的方法,但它在生态系统兼容性方面存在缺陷。

当您导入第三方模块时,Remix 会检查该包的package.json以查找"sideEffects": false。如果已配置,Remix 就会知道可以安全地从客户端包中删除代码。如果没有,导入将保留,因为代码可能依赖于模块的副作用(例如设置全局 polyfill 等)。

导入 ESM 包

本节内容仅适用于使用Classic Remix Compiler的情况。

您可能会尝试将仅 ESM 包导入您的应用程序,并在服务器渲染时看到类似这样的错误:

Error [ERR_REQUIRE_ESM]: require() of ES Module /app/node_modules/dot-prop/index.js from /app/project/build/index.js not supported.
Instead change the require of /app/project/node_modules/dot-prop/index.js in /app/project/build/index.js to a dynamic import() which is available in all CommonJS modules.

要解决此问题,请将 ESM 包添加到serverDependenciesToBundle选项中,该选项位于您的remix.config.js文件中。

在本例中,我们使用的是dot-prop包,因此我们将执行以下操作:

/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
  serverDependenciesToBundle: ["dot-prop"],
  // ...
};

为什么会出现这种情况?

Remix 将您的服务器构建编译为 CJS,并且不会打包您的 node 模块。CJS 模块无法导入 ESM 模块。

将包添加到serverDependenciesToBundle会告诉 Remix 将 ESM 模块直接打包到服务器构建中,而不是在运行时需要它。

ESM 不是未来吗?

是的!我们的计划是在服务器上允许您将应用程序编译为 ESM。但是,这将带来相反的问题,即无法导入与从 ESM 导入不兼容的某些 CommonJS 模块!因此,即使我们实现了这一目标,我们可能仍然需要此配置。

您可能会问我们为什么不只是将所有内容都打包到服务器上。我们可以做到,但这会减慢构建速度,并使生产堆栈跟踪都指向整个应用程序的单个文件。我们不想这样做。我们知道我们可以最终解决这个问题,而无需做出这种权衡。

随着主要的部署平台现在支持 ESM 服务器端,我们相信未来比过去更光明。我们仍在努力为 ESM 服务器构建提供可靠的开发体验,我们当前的方法依赖于您无法在 ESM 中执行的某些操作。我们会实现的。

CSS 包被错误地进行树摇

本节内容仅适用于使用Classic Remix Compiler的情况。

当将CSS 包裹功能export *结合使用(例如,当使用像components/index.ts这样的索引文件时,该文件从所有子目录中重新导出)时,您可能会发现从重新导出的模块中缺少的样式构建输出。

这是由于esbuild的 CSS 树摇问题。作为解决方法,您应该使用命名重新导出。

- export * from "./Button";
+ export { Button } from "./Button";

请注意,即使不存在此问题,我们仍然建议使用命名重新导出!虽然这可能会引入更多样板代码,但您可以明确控制模块的公共接口,而不是无意中公开所有内容。

文档和示例根据 MIT