升级到 v2
本文档提供了使用 Classic Remix 编译器 从 v1 迁移到 v2 的指南。有关迁移到 Vite 的更多指南,请参阅 Remix Vite 文档。
所有 v2 API 和行为在 v1 中都可通过 Future Flags 获得。可以一次启用一个,以避免项目开发中断。启用所有标志后,升级到 v2 应该是一次不间断的升级。
如果您遇到问题,请参阅 Troubleshooting 部分。
要快速了解一些常见的升级问题,请查看 🎥 2 分钟升级到 v2。
remix dev
有关配置选项,请参阅 remix dev
docs。
remix-serve
如果您使用的是 Remix App Server (remix-serve
),请启用 v2_dev
:
/** @type {import('@remix-run/dev').AppConfig} */module.exports = { future: { v2_dev: true, },};
就是这样!
自定义应用服务器
如果您使用自己的应用服务器(server.js
),
请查看我们的 模板,了解如何与 v2_dev
集成的示例
或按照以下步骤操作:
- 启用
v2_dev
:
/** @type {import('@remix-run/dev').AppConfig} */module.exports = {future: {v2_dev: true,},};
- 更新
package.json
中的scripts
:
- 用
remix dev
替换所有remix watch
- 删除多余的
NODE_ENV=development
- 使用
-c
/--command
运行您的应用服务器
例如:
{"scripts": { "dev:remix": "cross-env NODE_ENV=development remix watch", "dev:server": "cross-env NODE_ENV=development node ./server.js" "dev": "remix dev -c 'node ./server.js'",}}
- 应用运行时,向 Remix 编译器发送
ready
消息
// server.js lines=[1-2,11]import { broadcastDevReady } from "@remix-run/node";// 从`@remix-run/cloudflare`导入 { logDevReady } // 如果使用 CloudFlare,请使用 `logDevReady`
const BUILD_DIR = path.join(process.cwd(), "build");
// ... 设置服务器的代码放在这里 ...
const port = 3000;app.listen(port, async () => {console.log(`👉 http://localhost:${port}`);broadcastDevReady(await import(BUILD_DIR));});
- (可选)
--manual
如果您依赖于 require
缓存清除,则可以使用 --manual
标志继续执行此操作:
remix dev --manual -c 'node ./server.js'
查看 手动模式指南 了解更多详情。
从 v1 升级到 v2 后
在 v1 中启用 future.v2_dev
标志并使其正常工作后,您就可以升级到 v2 了。
如果您刚刚将 v2_dev
设置为 true
,则可以将其删除,一切应该都可以正常工作。
如果您正在使用 v2_dev
配置,则需要将其移动到 dev
配置字段:
/** @type {import('@remix-run/dev').AppConfig} */ module.exports = {- future: {- v2_dev: {- port: 4004- }- }+ dev: {+ port: 4004+ } }
文件系统路由约定
升级而不更改文件
如果您现在不想进行更改(或者永远不想,这只是一个约定,您可以使用您喜欢的任何文件组织),那么即使升级到 v2 后,您也可以继续使用旧约定 @remix-run/v1-route-convention
。
npm i -D @remix-run/v1-route-convention
const { createRoutesFromFolders,} = require("@remix-run/v1-route-convention");
/** @type {import('@remix-run/dev').AppConfig} */module.exports = { future: { // makes the warning go away in v1.15+ v2_routeConvention: true, },
routes(defineRoutes) { // uses the v1 convention, works in v1.15+ and v2 return createRoutesFromFolders(defineRoutes); },};
升级到新约定
- 路由嵌套现在由文件名中的点(
.
)创建,而不是文件夹嵌套 - 段中的
后缀_
下划线选择退出嵌套,使用可能匹配的父路由,而不是点(.
)。 - 段中的
_前缀
下划线创建没有路径的布局路由,而不是__double
下划线前缀。 _index.tsx
文件创建索引路由,而不是index.tsx
在 v1 中,路由文件夹如下所示:
app/├── routes/│ ├── __auth/│ │ ├── login.tsx│ │ ├── logout.tsx│ │ └── signup.tsx│ ├── __public/│ │ ├── about-us.tsx│ │ ├── contact.tsx│ │ └── index.tsx│ ├── dashboard/│ │ ├── calendar/│ │ │ ├── $day.tsx│ │ │ └── index.tsx│ │ ├── projects/│ │ │ ├── $projectId/│ │ │ │ ├── collaborators.tsx│ │ │ │ ├── edit.tsx│ │ │ │ ├── index.tsx│ │ │ │ ├── settings.tsx│ │ │ │ └── tasks.$taskId.tsx│ │ │ ├── $projectId.tsx│ │ │ └── new.tsx│ │ ├── calendar.tsx│ │ ├── index.tsx│ │ └── projects.tsx│ ├── __auth.tsx│ ├── __public.tsx│ └── dashboard.projects.$projectId.print.tsx└── root.tsx
使用 v2_routeConvention
后变为这样:
app/├── routes/│ ├── _auth.login.tsx│ ├── _auth.logout.tsx│ ├── _auth.signup.tsx│ ├── _auth.tsx│ ├── _public._index.tsx│ ├── _public.about-us.tsx│ ├── _public.contact.tsx│ ├── _public.tsx│ ├── dashboard._index.tsx│ ├── dashboard.calendar._index.tsx│ ├── dashboard.calendar.$day.tsx│ ├── dashboard.calendar.tsx│ ├── dashboard.projects.$projectId._index.tsx│ ├── dashboard.projects.$projectId.collaborators.tsx│ ├── dashboard.projects.$projectId.edit.tsx│ ├── dashboard.projects.$projectId.settings.tsx│ ├── dashboard.projects.$projectId.tasks.$taskId.tsx│ ├── dashboard.projects.$projectId.tsx│ ├── dashboard.projects.new.tsx│ ├── dashboard.projects.tsx│ └── dashboard_.projects.$projectId.print.tsx└── root.tsx
请注意,父路由现在被分组在一起,而不是在它们之间有几十个路由(如 auth 路由)。具有相同路径但嵌套不同的路由(如 dashboard
和 dashboard_
)也分组在一起。
使用新约定,任何路由都可以是一个目录,其中包含一个 route.tsx
文件来定义路由模块。这使得模块与它们所使用的路由可以共置:
例如,我们可以将 _public.tsx
移动到 _public/route.tsx
,然后将路由使用的模块共置:
app/├── routes/│ ├── _auth.tsx│ ├── _public/│ │ ├── footer.tsx│ │ ├── header.tsx│ │ └── route.tsx│ ├── _public._index.tsx│ ├── _public.about-us.tsx│ └── etc.└── root.tsx
有关此更改的更多背景信息,请参阅 原始 扁平路由
提案。
路由 headers
在 Remix v2 中,路由 headers
函数的行为略有变化。您可以通过 remix.config.js
中的 future.v2_headers
标志提前选择加入此新行为。
在 v1 中,Remix 仅使用叶子 渲染
路由 headers
函数的结果。您有责任向每个潜在叶子添加 headers
函数并相应地合并到 parentHeaders
中。这会很快变得乏味,而且当您添加新路由时也很容易忘记添加 headers
函数,即使您希望它只与其父路由共享相同的标头。
在 v2 中,Remix 现在使用它在渲染的路由中找到的最深 headers
函数。这让您可以更轻松地跨来自共同祖先的路由共享标头。然后,您可以根据需要将 headers
函数添加到更深的路由(如果它们需要特定行为)。
路由 meta
在 Remix v2 中,路由 meta
函数的签名以及 Remix 在后台处理元标记的方式已发生变化。
现在,您将返回一个描述符数组并自行管理合并,而不是从 meta
返回对象。这使 meta
API 更接近 links
,并且允许更灵活地控制元标记的呈现方式。
此外,<Meta />
将不再为层次结构中的每个路由呈现元数据。只会呈现从叶路由中的 meta
返回的数据。您仍然可以通过访问 函数参数中的 matches
选择从父路由包含元数据。
有关此更改的更多背景信息,请参阅 原始 v2 meta
提案。
在 v2 中使用 v1 meta
约定
您可以使用 @remix-run/v1-meta
包更新您的 meta
导出以继续使用 v1 约定。
使用 metaV1
函数,您可以传入 meta
函数的参数和它当前返回的相同对象。此函数将使用相同的合并逻辑将叶路由的元数据与其 直接父路由 元数据合并,然后将其转换为可在 v2 中使用的元描述符数组。
export function meta() { return { title: "...", description: "...", "og:title": "...", };}
// app/routes/v2-route.tsx goodimport { metaV1 } from "@remix-run/v1-meta";
export function meta(args) { return metaV1(args, { title: "...", description: "...", "og:title": "...", });}
需要注意的是,默认情况下,此函数不会在整个层次结构中合并元数据。这是因为您可能有一些路由直接返回对象数组,而没有 metaV1
函数,这可能会导致不可预测的行为。如果您想在整个层次结构中合并元数据,请对所有路由的元数据导出使用 metaV1
函数。
parentsData
参数
在 v2 中,meta
函数不再接收 parentsData
参数。这是因为 meta
现在可以通过 matches
参数 访问您的所有路由匹配项,其中包括每个匹配项的加载器数据。
为了复制 parentsData
的 API,@remix-run/v1-meta
包提供了一个 getMatchesData
函数。它返回一个对象,其中每个匹配项的数据都由路由的 ID 键入。
export function meta(args) { const parentData = args.parentsData["routes/parent"];}
变成:
// app/routes/v2-route.tsx goodimport { getMatchesData } from "@remix-run/v1-meta";
export function meta(args) { const matchesData = getMatchesData(args); const parentData = matchesData["routes/parent"];}
更新到新的 meta
export function meta() { return { title: "...", description: "...", "og:title": "...", };}
// app/routes/v2-route.tsx goodexport function meta() { return [ { title: "..." }, { name: "description", content: "..." }, { property: "og:title", content: "..." },
// you can now add SEO related <links> { tagName: "link", rel: "canonical", href: "..." },
// and <script type=ld+json> { "script:ld+json": { some: "value", }, }, ];}
matches
参数
请注意,在 v1 中,嵌套路由返回的对象都已合并,您现在需要使用 matches
自行管理合并:
// app/routes/v2-route.tsx goodexport function meta({ matches }) { const rootMeta = matches[0].meta; const title = rootMeta.find((m) => m.title);
return [ title, { name: "description", content: "..." }, { property: "og:title", content: "..." },
// you can now add SEO related <links> { tagName: "link", rel: "canonical", href: "..." },
// and <script type=ld+json> { "script:ld+json": { "@context": "https://schema.org", "@type": "Organization", name: "Remix", }, }, ];}
meta 文档有更多关于合并路由元数据的提示。
CatchBoundary
和 ErrorBoundary
/** @type {import('@remix-run/dev').AppConfig} */module.exports = { future: { v2_errorBoundary: true, },};
在 v1 中,抛出的 Response
会渲染最近的 CatchBoundary
,而所有其他未处理的异常都会渲染 ErrorBoundary
。在 v2 中没有 CatchBoundary
,所有未处理的异常都会渲染 ErrorBoundary
、响应或其他。
此外,错误不再作为 props 传递给 ErrorBoundary
,而是通过 useRouteError
钩子访问。
import { useCatch } from "@remix-run/react";
export function CatchBoundary() { const caught = useCatch();
return ( <div> <h1>Oops</h1> <p>Status: {caught.status}</p> <p>{caught.data.message}</p> </div> );}
export function ErrorBoundary({ error }) { console.error(error); return ( <div> <h1>Uh oh ...</h1> <p>Something went wrong</p> <pre>{error.message || "Unknown error"}</pre> </div> );}
变成:
// app/routes/v2-route.tsx goodimport { useRouteError, isRouteErrorResponse,} from "@remix-run/react";
export function ErrorBoundary() { const error = useRouteError();
// when true, this is what used to go to `CatchBoundary` if (isRouteErrorResponse(error)) { return ( <div> <h1>Oops</h1> <p>Status: {error.status}</p> <p>{error.data.message}</p> </div> ); }
// Don't forget to typecheck with your own logic. // Any value can be thrown, not just errors! let errorMessage = "Unknown error"; if (isDefinitelyAnError(error)) { errorMessage = error.message; }
return ( <div> <h1>Uh oh ...</h1> <p>Something went wrong.</p> <pre>{errorMessage}</pre> </div> );}
formMethod
/** @type {import('@remix-run/dev').AppConfig} */module.exports = { future: { v2_normalizeFormMethod: true, },};
多个 API 返回提交的 formMethod
。在 v1 中,它们返回方法的小写版本,但在 v2 中,它们返回大写版本。这是为了使其符合 HTTP 和 fetch
规范。
function Something() { const navigation = useNavigation();
// v1 navigation.formMethod === "post";
// v2 navigation.formMethod === "POST";}
export function shouldRevalidate({ formMethod }) { // v1 formMethod === "post";
// v2 formMethod === "POST";}
useTransition
此钩子现在称为 useNavigation
,以避免与最近同名的 React 钩子混淆。它也不再具有 type
字段,并将 submission
对象扁平化为 navigation
对象本身。
import { useTransition } from "@remix-run/react";
function SomeComponent() { const transition = useTransition(); transition.submission.formData; transition.submission.formMethod; transition.submission.formAction; transition.type;}
// app/routes/v2-route.tsx goodimport { useNavigation } from "@remix-run/react";
function SomeComponent() { const navigation = useNavigation();
// transition.submission keys are flattened onto `navigation[key]` navigation.formData; navigation.formMethod; navigation.formAction;
// this key is removed navigation.type;}
您可以使用以下示例推导先前的 transition.type
。请记住,可能有一种更简单的方法来获得相同的行为,通常检查 navigation.state
、navigation.formData
或使用 useActionData
的操作返回的数据可以获得您想要的用户体验。欢迎在 Discord 中向我们提问,我们会帮助您 :D
function Component() { const navigation = useNavigation();
// transition.type === "actionSubmission" const isActionSubmission = navigation.state === "submitting";
// transition.type === "actionReload" const isActionReload = navigation.state === "loading" && navigation.formMethod != null && navigation.formMethod != "GET" && // We had a submission navigation and are loading the submitted location navigation.formAction === navigation.location.pathname;
// transition.type === "actionRedirect" const isActionRedirect = navigation.state === "loading" && navigation.formMethod != null && navigation.formMethod != "GET" && // We had a submission navigation and are now navigating to different location navigation.formAction !== navigation.location.pathname;
// transition.type === "loaderSubmission" const isLoaderSubmission = navigation.state === "loading" && navigation.state.formMethod === "GET" && // We had a loader submission and are navigating to the submitted location navigation.formAction === navigation.location.pathname;
// transition.type === "loaderSubmissionRedirect" const isLoaderSubmissionRedirect = navigation.state === "loading" && navigation.state.formMethod === "GET" && // We had a loader submission and are navigating to a new location navigation.formAction !== navigation.location.pathname;}
关于 GET 提交的说明
在 Remix v1 中,GET 提交(例如 <Form method="get">
或 submit({}, { method: 'get' })
)在 transition.state
中从 idle -> submitting -> idle
变为了。这在语义上并不完全正确,因为即使您正在 提交
表单,您也正在执行 GET 导航并且只执行加载程序(而不是操作)。从功能上讲,它与 <Link>
或 navigate()
没有什么不同,只是用户可能通过输入指定搜索参数值。
在 v2 中,GET 提交更准确地反映为加载导航,因此变为 idle -> loading -> idle
,以使 navigation.state
与正常链接的行为保持一致。如果您的 GET 提交来自 <Form>
或 submit()
,则将填充 useNavigation.form*
,因此您可以根据需要进行区分。
useFetcher
与 useNavigation
类似,useFetcher
也扁平化了 submission
并删除了 type
字段。
import { useFetcher } from "@remix-run/react";
function SomeComponent() { const fetcher = useFetcher(); fetcher.submission.formData; fetcher.submission.formMethod; fetcher.submission.formAction; fetcher.type;}
// app/routes/v2-route.tsx goodimport { useFetcher } from "@remix-run/react";
function SomeComponent() { const fetcher = useFetcher();
// these keys are flattened fetcher.formData; fetcher.formMethod; fetcher.formAction;
// this key is removed fetcher.type;}
您可以使用以下示例推导先前的 fetcher.type
。请记住,可能有一种更简单的方法来获得相同的行为,通常检查 fetcher.state
、fetcher.formData
或从 fetcher.data
上的操作返回的数据可以获得您想要的用户体验。欢迎在 Discord 中询问我们,我们会帮助您 :D
function Component() { const fetcher = useFetcher();
// fetcher.type === "init" const isInit = fetcher.state === "idle" && fetcher.data == null;
// fetcher.type === "done" const isDone = fetcher.state === "idle" && fetcher.data != null;
// fetcher.type === "actionSubmission" const isActionSubmission = fetcher.state === "submitting";
// fetcher.type === "actionReload" const isActionReload = fetcher.state === "loading" && fetcher.formMethod != null && fetcher.formMethod != "GET" && // If we returned data, we must be reloading fetcher.data != null;
// fetcher.type === "actionRedirect" const isActionRedirect = fetcher.state === "loading" && fetcher.formMethod != null && fetcher.formMethod != "GET" && // If we have no data we must have redirected fetcher.data == null;
// fetcher.type === "loaderSubmission" const isLoaderSubmission = fetcher.state === "loading" && fetcher.formMethod === "GET";
// fetcher.type === "normalLoad" const isNormalLoad = fetcher.state === "loading" && fetcher.formMethod == null;}
关于 GET 提交的说明
在 Remix v1 中,GET 提交(例如 <fetcher.Form method="get">
或 fetcher.submit({}, { method: 'get' })
)在 fetcher.state
中从 idle -> submitting -> idle
变为了。这在语义上并不完全正确,因为即使您正在 提交
表单,您也正在执行 GET 请求并且只执行加载器(而不是操作)。从功能上讲,它与 fetcher.load()
没有什么不同,只是用户可能通过输入指定搜索参数值。
在 v2 中,GET 提交更准确地反映为加载请求,因此进入 idle -> loading -> idle
以使 fetcher.state
与正常 fetcher 加载的行为保持一致。如果您的 GET 提交来自 <fetcher.Form>
或 fetcher.submit()
,则 fetcher.form*
将被填充,因此您可以根据需要进行区分。
链接 imagesizes
和 imagesrcset
路由 links
属性应全部为 React 驼峰式大小写值,而不是 HTML 小写值。这两个值在 v1 中以小写形式出现。在 v2 中,只有驼峰式大小写版本有效:
export const links: LinksFunction = () => { return [ { rel: "preload", as: "image", imagesrcset: "...", imagesizes: "...", }, ];};
export const links: V2_LinksFunction = () => { return [ { rel: "preload", as: "image", imageSrcSet: "...", imageSizes: "...", }, ];};
browserBuildDirectory
在您的 remix.config.js
中,将 browserBuildDirectory
重命名为 assetsBuildDirectory
。
// remix.config.js lines=[3]/** @type {import('@remix-run/dev').AppConfig} */module.exports = { browserBuildDirectory: "./public/build",};
// remix.config.js good lines=[3]/** @type {import('@remix-run/dev').AppConfig} */module.exports = { assetsBuildDirectory: "./public/build",};
devServerBroadcastDelay
从您的 remix.config.js
中删除 devServerBroadcastDelay
,因为需要此选项的竞争条件已在 v2 或 v2_dev
中消除。
/** @type {import('@remix-run/dev').AppConfig} */ module.exports = { devServerBroadcastDelay: 300, };
devServerPort
在您的 remix.config.js
中,将 devServerPort
重命名为 future.v2_dev.port
。
// remix.config.js lines=[3]/** @type {import('@remix-run/dev').AppConfig} */module.exports = { devServerPort: 8002,};
// remix.config.js good lines=[3-7]/** @type {import('@remix-run/dev').AppConfig} */module.exports = { // While on v1.x, this is via a future flag future: { v2_dev: { port: 8002, }, },};
从 v1 升级到 v2 后,这将变为 根级 dev
配置。
serverBuildDirectory
在 remix.config.js
中,将 serverBuildDirectory
重命名为 serverBuildPath
,并指定模块路径,而不是目录。
// remix.config.js lines=[3]/** @type {import('@remix-run/dev').AppConfig} */module.exports = { serverBuildDirectory: "./build",};
// remix.config.js good lines=[3]/** @type {import('@remix-run/dev').AppConfig} */module.exports = { serverBuildPath: "./build/index.js",};
Remix 过去会为服务器创建多个模块,但现在会创建一个文件。
serverBuildTarget
无需指定构建目标,而是使用 remix.config.js
选项来生成服务器目标所需的服务器构建。此更改允许 Remix 部署到更多 JavaScript 运行时、服务器和主机,而无需 Remix 源代码了解它们。
以下配置应替换您当前的 serverBuildTarget
:
arc
/** @type {import('@remix-run/dev').AppConfig} */module.exports = { publicPath: "/_static/build/", serverBuildPath: "server/index.js", serverMainFields: ["main", "module"], // default value, can be removed serverMinify: false, // default value, can be removed serverModuleFormat: "cjs", // default value in 1.x, add before upgrading serverPlatform: "node", // default value, can be removed};
cloudflare-pages
/** @type {import('@remix-run/dev').AppConfig} */module.exports = { publicPath: "/build/", // default value, can be removed serverBuildPath: "functions/[[path]].js", serverConditions: ["worker"], serverDependenciesToBundle: "all", serverMainFields: ["browser", "module", "main"], serverMinify: true, serverModuleFormat: "esm", // default value in 2.x, can be removed once upgraded serverPlatform: "neutral",};
cloudflare-workers
/** @type {import('@remix-run/dev').AppConfig} */module.exports = { publicPath: "/build/", // default value, can be removed serverBuildPath: "build/index.js", // default value, can be removed serverConditions: ["worker"], serverDependenciesToBundle: "all", serverMainFields: ["browser", "module", "main"], serverMinify: true, serverModuleFormat: "esm", // default value in 2.x, can be removed once upgraded serverPlatform: "neutral",};
deno
/** @type {import('@remix-run/dev').AppConfig} */module.exports = { publicPath: "/build/", // default value, can be removed serverBuildPath: "build/index.js", // default value, can be removed serverConditions: ["deno", "worker"], serverDependenciesToBundle: "all", serverMainFields: ["module", "main"], serverMinify: false, // default value, can be removed serverModuleFormat: "esm", // default value in 2.x, can be removed once upgraded serverPlatform: "neutral",};
node-cjs
版本
/** @type {import('@remix-run/dev').AppConfig} */module.exports = { publicPath: "/build/", // default value, can be removed serverBuildPath: "build/index.js", // default value, can be removed serverMainFields: ["main", "module"], // default value, can be removed serverMinify: false, // default value, can be removed serverModuleFormat: "cjs", // default value in 1.x, add before upgrading serverPlatform: "node", // default value, can be removed};
serverModuleFormat
默认服务器模块输出格式已从 cjs
更改为 esm
。您可以在 v2 中继续使用 CJS,但应用中的许多依赖项可能与 ESM 不兼容。
在 remix.config.js
中,您应该指定 serverModuleFormat: "cjs"
以保留现有行为,或指定 serverModuleFormat: "esm"
以选择新行为。
browserNodeBuiltinsPolyfill
默认情况下,浏览器不再提供 Node.js 内置模块的 Polyfill。在 Remix v2 中,您需要根据需要明确重新引入任何 polyfill(或空白 polyfill):
/** @type {import('@remix-run/dev').AppConfig} */module.exports = { browserNodeBuiltinsPolyfill: { modules: { buffer: true, fs: "empty", }, globals: { Buffer: true, }, },};
尽管我们建议明确说明浏览器软件包中允许使用哪些 polyfill,尤其是由于某些 polyfill 可能非常大,但您可以使用以下配置快速恢复 Remix v1 中的全套 polyfill:
const { builtinModules } = require("node:module");
/** @type {import('@remix-run/dev').AppConfig} */module.exports = { browserNodeBuiltinsPolyfill: { modules: builtinModules, },};
serverNodeBuiltinsPolyfill
对于非 Node.js 服务器平台,不再默认提供 Node.js 内置模块的 Polyfill。
如果您的目标是非 Node.js 服务器平台,并且想要选择 v1 中的新默认行为,则在 remix.config.js
中,您应该首先通过为 serverNodeBuiltinsPolyfill.modules
明确提供一个空对象来删除所有服务器 polyfill:
/** @type {import('@remix-run/dev').AppConfig} */module.exports = { serverNodeBuiltinsPolyfill: { modules: {}, },};
然后,您可以根据需要重新引入任何 polyfill(或空白 polyfill)。
/** @type {import('@remix-run/dev').AppConfig} */module.exports = { serverNodeBuiltinsPolyfill: { modules: { buffer: true, fs: "empty", }, globals: { Buffer: true, }, },};
作为参考,可以手动指定 v1 中的完整默认 polyfill 集,如下所示:
/** @type {import('@remix-run/dev').AppConfig} */module.exports = { serverNodeBuiltinsPolyfill: { modules: { _stream_duplex: true, _stream_passthrough: true, _stream_readable: true, _stream_transform: true, _stream_writable: true, assert: true, "assert/strict": true, buffer: true, console: true, constants: true, crypto: "empty", diagnostics_channel: true, domain: true, events: true, fs: "empty", "fs/promises": "empty", http: true, https: true, module: true, os: true, path: true, "path/posix": true, "path/win32": true, perf_hooks: true, process: true, punycode: true, querystring: true, stream: true, "stream/promises": true, "stream/web": true, string_decoder: true, sys: true, timers: true, "timers/promises": true, tty: true, url: true, util: true, "util/types": true, vm: true, wasi: true, worker_threads: true, zlib: true, }, },};
installGlobals
为了准备使用 Node 内置的 fetch 实现,安装 fetch 全局变量现在是应用服务器的责任。如果您使用的是 remix-serve
,则无需执行任何操作。如果您使用的是自己的应用服务器,则需要自行安装全局变量。
import { installGlobals } from "@remix-run/node";
installGlobals();
删除导出的 polyfill
Remix v2 也不再从 @remix-run/node
导出这些 polyfill 实现,而您应该只使用全局命名空间中的实例。一个可能出现并需要更改的地方是您的 app/entry.server.tsx
文件,您还需要通过 createReadableStreamFromReadable
将 Node PassThrough
转换为 web ReadableStream
:
import { PassThrough } from "node:stream"; import type { AppLoadContext, EntryContext } from "@remix-run/node"; // or cloudflare/deno import { Response } from "@remix-run/node"; // or cloudflare/deno import { createReadableStreamFromReadable } from "@remix-run/node"; // or cloudflare/deno import { RemixServer } from "@remix-run/react"; import { isbot } from "isbot"; import { renderToPipeableStream } from "react-dom/server";
const ABORT_DELAY = 5_000;
export default function handleRequest({ /* ... */ }) { ... }
function handleBotRequest(...) { return new Promise((resolve, reject) => { let shellRendered = false; const { pipe, abort } = renderToPipeableStream( <RemixServer ... />, { onAllReady() { shellRendered = true; const body = new PassThrough();
responseHeaders.set("Content-Type", "text/html");
resolve( new Response(body, { new Response(createReadableStreamFromReadable(body), { headers: responseHeaders, status: responseStatusCode, }) );
pipe(body); }, ... onShellError(error: unknown) { ... } onError(error: unknown) { ... } } );
setTimeout(abort, ABORT_DELAY); }); }
function handleBrowserRequest(...) { return new Promise((resolve, reject) => { let shellRendered = false; const { pipe, abort } = renderToPipeableStream( <RemixServer ... />, { onShellReady() { shellRendered = true; const body = new PassThrough();
responseHeaders.set("Content-Type", "text/html");
resolve( new Response(body, { new Response(createReadableStreamFromReadable(body), { headers: responseHeaders, status: responseStatusCode, }) );
pipe(body); }, onShellError(error: unknown) { ... }, onError(error: unknown) { ... }, } );
setTimeout(abort, ABORT_DELAY); }); }
source-map-support
源地图支持现在是应用服务器的责任。如果您使用的是 remix-serve
,则无需执行任何操作。如果您使用的是自己的应用服务器,则需要自行安装 source-map-support
。
npm i source-map-support
import sourceMapSupport from "source-map-support";
sourceMapSupport.install();
Netlify 适配器
@remix-run/netlify
运行时适配器已被弃用,取而代之的是
@netlify/remix-adapter
和 @netlify/remix-edge-adapter
并且从 Remix v2 开始已被删除。请更新您的代码,将所有 @remix-run/netlify
导入更改为
@netlify/remix-adapter
。
请记住,@netlify/remix-adapter
需要 @netlify/functions@^1.0.0
,与 @remix-run/netlify
中当前支持的 @netlify/functions
版本相比,这是一个重大变化。
由于删除了此适配器,我们还删除了我们的 Netlify 模板,转而使用 官方 Netlify 模板。
Vercel 适配器
@remix-run/vercel
运行时适配器已被弃用,转而使用开箱即用的 Vercel 功能,并且现在已从 Remix v2 中删除。请通过从您的
package.json
中删除 @remix-run/vercel
和 @vercel/node
、删除您的 server.js
/server.ts
文件以及从您的 remix.config.js
中删除 server
和 serverBuildPath
选项来更新您的代码。
由于删除了此适配器,我们还删除了我们的 Vercel 模板,转而使用 官方 Vercel 模板。
内置 PostCSS/Tailwind 支持
在 v2 中,如果您的项目中存在 PostCSS 和 / 或 Tailwind 配置文件,则这些工具会自动在 Remix 编译器中使用。
如果您在 Remix 之外有自定义 PostCSS 和 / 或 Tailwind 设置,并且希望在迁移到 v2 时保留这些设置,则可以在 remix.config.js
中禁用这些功能。
/** @type {import('@remix-run/dev').AppConfig} */module.exports = { postcss: false, tailwind: false,};
故障排除
ESM / CommonJS 错误
"SyntaxError: Named export '<something>' not found. The requested module '<something>' is a CommonJS module, which may not support all module.exports as named exports."
请参阅 serverModuleFormat
部分。