陷阱
On this page

陷阱

¥Gotchas

使用 React 在服务器和浏览器中渲染你的应用存在一些固有的陷阱。此外,在构建 Remix 的过程中,我们一直专注于生产结果和可扩展性。存在一些开发者经验和生态系统兼容性问题,我们尚未解决。

¥Rendering your app on the server and in the browser with React has some inherent gotchas. Additionally, as we've built Remix, we've been laser focused on production results and scalability. Some developer-experience and ecosystem-compatibility issues exist that we haven't smoothed over yet.

本文档应该可以帮助你克服这些障碍。

¥This document should help you get over these bumps.

typeof window 检查

¥typeof window checks

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

¥Because the same JavaScript code can run in the browser as well as the server, sometimes you need to have a part of your code that only runs in one context or the other:

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

这在 Node.js 环境中运行良好,但是某些环境可能实际上支持 window(例如 Deno v1),因此如果你确实想检查是否在浏览器中运行,最好检查 document

¥This works fine in a Node.js environment, however, some environments might actually support window (like Deno v1), so if you really want to check whether you're running in the browser, it's better to check for document instead:

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

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

¥This will work for all JS environments (Node.js, Deno, Workers, etc.).

浏览器扩展注入代码

¥Browser extensions injecting code

你可能会在浏览器中遇到以下警告:

¥You may run into this warning in the browser:

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

这是来自 React 的 hydration 警告,很可能是因为你的某个浏览器扩展程序将脚本注入了服务器渲染的 HTML,从而与最终的 HTML 产生了差异。

¥This is a hydration warning from React, and is most likely due to one of your browser extensions injecting scripts into the server-rendered HTML, creating a difference with the resulting HTML.

在隐身模式下查看页面,警告应该会消失。

¥Check out the page in incognito mode, the warning should disappear.

loader 中写入会话

¥Writing to Sessions in loaders

通常,你应该只在操作中写入会话,但在某些情况下,在加载器中写入也是有意义的(例如匿名用户、导航跟踪等)。

¥Typically, you should only write to sessions in actions, but there are occasions where it makes sense in loaders (anonymous users, navigation tracking, etc.)

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

¥While multiple loaders can read from the same session, writing to a session in loaders can cause problems.

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

¥Remix loaders run in parallel, and sometimes in separate requests (client transitions call fetch for each loader). If one loader is writing to a session while another is attempting to read from it, you will hit bugs and/or non-deterministic behavior.

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

¥Additionally, sessions are built on cookies which come from the browser's request. After committing a session, it goes to the browser in a Set-Cookie header which is then sent back to the server on the next request in the Cookie header. Regardless of parallel loaders, you can't write to a cookie with Set-Cookie and then attempt to read it from the original request Cookie and expect updated values. It needs to make a round trip to the browser first and come from the next request.

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

¥If you need to write to a session in a loader, ensure the loader doesn't share that session with any other loaders.

客户端包中的服务器代码

¥Server Code in Client Bundles

本节仅在你使用 Classic Remix 编译器 时适用。

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

¥You may run into this strange error in the browser. It almost always means that server code made it into browser bundles.

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

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

¥For example, you can't import fs-extra directly into a route module:

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 目录下创建了一个新文件:

¥To fix it, move the import into a different module named *.server.ts or *.server.js and import from there. In our example here, we create a new file at utils/fs-extra.server.ts:

export { default } from "fs-extra";

然后,将路由中的导入更改为新的 "wrapper" 模块:

¥And then change our import in the route to the new "wrapper" module:

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 中,这样使用 tree shake 的打包工具就知道他们可以安全地从浏览器打包中移除代码。

¥Even better, send a PR to the project to add "sideEffects": false to their package.json so that bundlers that tree shake know they can safely remove the code from browser bundles.

同样,如果你在路由模块的顶层作用域中调用依赖于服务器端代码的函数,也可能会遇到同样的错误。

¥Similarly, you may run into the same error if you call a function at the top-level scope of your route module that depends on server-only code.

例如,Remix 上传处理程序,例如 unstable_createFileUploadHandlerunstable_createMemoryUploadHandler 在后台使用 Node 全局变量,因此只能在服务器上调用。你可以在 *.server.ts*.server.js 文件中调用这些函数,也可以将它们移动到路由的 actionloader 函数中。

¥For example, Remix upload handlers like unstable_createFileUploadHandler and unstable_createMemoryUploadHandler use Node globals under the hood and should only be called on the server. You can call either of these functions in a *.server.ts or *.server.js file, or you can move them into your route's action or loader function.

所以不要这样做:

¥So instead of doing:

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 ...
}

你应该这样做:

¥You should be doing:

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 ...
}

为什么会这样?

¥Why does this happen?

Remix 使用 "树摇" 从浏览器 bundle 中删除服务器代码。路由模块 actionheadersloader 导出的所有内容都将被删除。这是一个很好的方法,但存在生态系统兼容性问题。

¥Remix uses "tree shaking" to remove server code from browser bundles. Anything inside Route module action, headers, and loader exports will be removed. It's a great approach but suffers from ecosystem compatibility.

导入第三方模块时,Remix 会检查该包的 package.json 是否包含 "sideEffects": false。如果已配置此选项,Remix 便可以安全地从客户端 bundle 中移除代码。如果没有它,导入仍然保留,因为代码可能依赖于模块的副作用(例如设置全局 polyfill 等)。

¥When you import a third-party module, Remix checks the package.json of that package for "sideEffects": false. If that is configured, Remix knows it can safely remove the code from the client bundles. Without it, the imports remain because code may depend on the module's side effects (like setting global polyfills, etc.).

导入 ESM 包

¥Importing ESM Packages

本节仅在你使用 Classic Remix 编译器 时适用。

你可以尝试将仅 ESM 的包导入到你的应用中,并在服务器渲染时看到类似这样的错误:

¥You may try importing an ESM-only package into your app and see an error like this when server rendering:

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 选项。

¥To fix it, add the ESM package to the serverDependenciesToBundle option in your remix.config.js file.

在本例中,我们使用 dot-prop 包,因此我们会这样做:

¥In our case here, we're using the dot-prop package, so we would do it like this:

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

为什么会这样?

¥Why does this happen?

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

¥Remix compiles your server build to CJS and doesn't bundle your node modules. CJS modules can't import ESM modules.

serverDependenciesToBundle 添加包会告知 Remix 将 ESM 模块直接打包到服务器构建中,而不是在运行时强制执行。

¥Adding packages to serverDependenciesToBundle tells Remix to bundle the ESM module directly into the server build instead of requiring it at runtime.

ESM 不是代表未来吗?

¥Isn't ESM the future?

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

¥Yes! Our plan is to allow you to compile your app to ESM on the server. However, that will come with the reverse problem of not being able to import some CommonJS modules that are incompatible with being imported from ESM! So even when we get there, we may still need this configuration.

你可能会问,为什么我们不直接把所有东西打包到服务器?我们可以,但这会减慢构建速度,并使整个应用的生产堆栈跟踪都指向一个文件。我们不想那样做。我们知道最终可以解决这个问题,而无需进行这种权衡。

¥You may ask why we don't just bundle everything for the server. We could, but that will slow down builds and make production stack traces all point to a single file for your entire app. We don't want to do that. We know we can smooth this over eventually without making that tradeoff.

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

¥With major deployment platforms now supporting ESM server side, we're confident the future is brighter than the past here. We're still working on a solid dev experience for ESM server builds, our current approach relies on some things that you can't do in ESM. We'll get there.

CSS 打包不正确摇树优化

¥CSS bundle being incorrectly tree-shaken

本节仅在你使用 Classic Remix 编译器 时适用。

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

¥When using CSS bundling features in combination with export * (e.g., when using an index file like components/index.ts that re-exports from all subdirectories) you may find that styles from the re-exported modules are missing from the build output.

这是由于 esbuild 的 CSS tree shake 问题 造成的。作为一种解决方法,你应该使用命名的重新导出。

¥This is due to an issue with esbuild's CSS tree shaking. As a workaround, you should use named re-exports instead.

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

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

¥Note that, even if this issue didn't exist, we'd still recommend using named re-exports! While it may introduce a bit more boilerplate, you get explicit control over the module's public interface rather than inadvertently exposing everything.

Remix v2.17 中文网 - 粤ICP备13048890号