使用 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
中读取它并期望获得更新的值。它需要先进行往返到浏览器,然后才能从下一个请求中获取。
如果您需要在加载程序中写入会话,请确保加载程序不与任何其他加载程序共享该会话。
您可能会在浏览器中遇到这种奇怪的错误。几乎总是意味着服务器代码进入了浏览器包。
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_createFileUploadHandler
和unstable_createMemoryUploadHandler
在幕后使用 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 包添加到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 包裹功能与export *
结合使用(例如,当使用像components/index.ts
这样的索引文件时,该文件从所有子目录中重新导出)时,您可能会发现从重新导出的模块中缺少的样式构建输出。
这是由于esbuild
的 CSS 树摇问题。作为解决方法,您应该使用命名重新导出。
- export * from "./Button";
+ export { Button } from "./Button";
请注意,即使不存在此问题,我们仍然建议使用命名重新导出!虽然这可能会引入更多样板代码,但您可以明确控制模块的公共接口,而不是无意中公开所有内容。