使用 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
中写入会话通常,你只应该在 actions 中写入会话,但在 loaders 中这样做也有道理(匿名用户、导航跟踪等)
虽然多个 loaders 可以从同一个会话中读取,但在 loaders 中写入会话可能会导致问题。
Remix loaders 并行运行,有时在单独的请求中运行(客户端转换为每个 loader 调用 fetch
)。如果一个 loader 正在写入会话,而另一个 loader 正在尝试从中读取,你将遇到错误和/或不确定的行为。
此外,会话是基于浏览器请求中的 cookie 构建的。提交会话后,它会以 Set-Cookie
标头的形式发送到浏览器,然后在下一个请求中以 Cookie
标头的形式发送回服务器。无论并行加载器如何,您都不能使用 Set-Cookie
写入 cookie,然后尝试从原始请求的 Cookie
中读取它并期望得到更新后的值。它需要先往返浏览器,然后从下一个请求中获取。
如果需要在加载器中写入会话,请确保加载器不与任何其他加载器共享该会话。
您可能会在浏览器中遇到此奇怪的错误。它几乎总是意味着服务器代码进入了浏览器包。
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,在其 package.json
中添加 "sideEffects": false
,以便捆绑器可以进行摇树优化,从而知道可以安全地从浏览器包中删除该代码。
同样,如果您的路由模块的顶层作用域中调用了依赖于仅限服务器代码的函数,您可能会遇到相同的错误。
例如,像 unstable_createFileUploadHandler
和 unstable_createMemoryUploadHandler
这样的 Remix 上传处理程序在底层使用 Node 全局变量,因此只能在服务器上调用。您可以在 *.server.ts
或 *.server.js
文件中调用这些函数中的任何一个,或者将它们移动到路由的 action
或 loader
函数中。
因此,不要这样做:
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 使用“摇树优化”从浏览器包中删除服务器代码。路由模块 action
、headers
和 loader
导出中的任何内容都将被删除。这是一种很好的方法,但会受到生态系统兼容性的影响。
当您导入第三方模块时,Remix 会检查该包的 package.json
中是否包含 "sideEffects": false
。如果配置了此项,Remix 知道可以安全地从客户端包中删除该代码。如果没有它,导入将保留,因为代码可能依赖于模块的副作用(例如设置全局 polyfill 等)。
您可能会尝试将仅 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
包,因此我们将这样做:
/** @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 打包功能和 export *
(例如,当使用像 components/index.ts
这样的索引文件,该文件从所有子目录重新导出时),您可能会发现重新导出的模块的样式在构建输出中丢失。
这是由于 esbuild
的 CSS 摇树优化问题导致的。作为一种解决方法,您应该使用命名的重新导出。
- export * from "./Button";
+ export { Button } from "./Button";
请注意,即使不存在此问题,我们仍然建议使用命名的重新导出!虽然它可能会引入更多的样板代码,但您可以明确控制模块的公共接口,而不是无意中暴露所有内容。