コンテンツにスキップ

陷阱

このコンテンツはまだ日本語訳がありません。

使用 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 中写入 Session

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

虽然多个加载器可以从同一个会话中读取,但在加载器中向会话中写入可能会出现问题。

Remix 加载器并行运行,有时在单独的请求中运行(客户端转换调用每个加载器的 fetch)。如果一个加载器正在写入会话,而另一个加载器正在尝试从中读取,您将遇到错误和 / 或不确定的行为。

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

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

客户端 Bundles 中的服务器代码

本节仅当您使用 Classic Remix 编译器 时才相关。

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

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

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

app/routes/_index.tsx
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 处创建一个新文件:

app/utils/fs-extra.server.ts
export { default } from "fs-extra";

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

app/routes/_index.tsx
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 函数中。

因此不要这样做:

app/routes/some-route.tsx
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 ...
}

你应该这样做:

app/routes/some-route.tsx
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 编译器 时才相关。

您可以尝试将仅限 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 包添加到 remix.config.js 文件中的 serverDependenciesToBundle 选项中。

在我们的例子中,我们使用 dot-prop 包,因此我们可以这样做:

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

为什么会发生这种情况?

Remix 将您的服务器构建编译为 CJS,并且不会捆绑您的节点模块。CJS 模块无法导入 ESM 模块。

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

ESM 不是代表未来吗?

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

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

随着主流部署平台现在支持 ESM 服务器端,我们相信未来会比过去更加光明。我们仍在努力为 ESM 服务器构建提供可靠的开发体验,我们目前的方法依赖于一些您无法在 ESM 中完成的事情。我们会做到的。

CSS 包被错误地进行了 tree-shaking

本节仅当您使用 Classic Remix 编译器 时才相关。

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

这是由于 esbuild 的 CSS 树摇动问题 造成的。解决方法是,您应该改用命名重新导出。

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

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