remix dev
remix-serve
headers
meta
CatchBoundary
和 ErrorBoundary
formMethod
useTransition
useFetcher
imagesizes
和 imagesrcset
browserBuildDirectory
devServerBroadcastDelay
devServerPort
serverBuildDirectory
serverBuildTarget
serverModuleFormat
browserNodeBuiltinsPolyfill
serverNodeBuiltinsPolyfill
installGlobals
source-map-support
所有 v2 API 和行为在 v1 中都可以通过 Future Flags 获得。它们可以一次启用一个,以避免对您的项目造成开发中断。在您启用所有标志后,升级到 v2 应该是一个非破坏性的升级。
如果您遇到问题,请参阅问题排查部分。
要快速了解一些常见的升级问题,请查看 🎥 2 分钟升级到 v2。
remix dev
有关配置选项,请参阅remix dev
文档。
remix-serve
如果您正在使用 Remix 应用服务器 (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 watch
替换为 remix dev
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”消息
import { broadcastDevReady } from "@remix-run/node";
// import { logDevReady } from "@remix-run/cloudflare" // use `logDevReady` if using CloudFlare
const BUILD_DIR = path.join(process.cwd(), "build");
// ... code setting up your server goes here ...
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 中启用 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);
},
};
.
) 而不是文件夹嵌套来创建suffixed_
下划线的片段会选择不使用点 (.
) 与可能匹配的父路由进行嵌套。_prefixed
下划线的片段会创建没有路径的布局路由,而不是 __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
请注意,父路由现在被分组在一起,而不是它们之间有数十个路由(例如身份验证路由)。具有相同路径但嵌套不同的路由(例如 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
返回一个描述符数组并自己管理合并,而不是从 meta
返回一个对象。这使得 meta
API 更接近 links
,并且可以更灵活地控制 meta 标签的渲染方式。
此外,<Meta />
将不再为层次结构中的每个路由渲染 meta。只会渲染叶子路由中从 meta
返回的数据。您仍然可以选择通过访问函数参数中的 matches
来包含来自父路由的 meta。
有关此更改的更多背景信息,请参阅最初的 v2 meta
提案。
meta
约定您可以使用 @remix-run/v1-meta
包更新您的 meta
导出,以继续使用 v1 约定。
使用 metaV1
函数,您可以传入 meta
函数的参数以及它当前返回的相同对象。此函数将使用相同的合并逻辑来合并叶子路由的 meta 与其直接父路由 meta,然后再将其转换为 v2 中可用的 meta 描述符数组。
export function meta() {
return {
title: "...",
description: "...",
"og:title": "...",
};
}
import { metaV1 } from "@remix-run/v1-meta";
export function meta(args) {
return metaV1(args, {
title: "...",
description: "...",
"og:title": "...",
});
}
请务必注意,此函数默认情况下不会合并整个层次结构中的 meta。这是因为您可能有一些路由直接返回一个对象数组而没有 metaV1
函数,这可能会导致不可预测的行为。如果您想合并整个层次结构中的 meta,请为所有路由的 meta 导出使用 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"];
}
变为
import { 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": "...",
};
}
export 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
管理合并
export 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 文档提供了有关合并路由 meta 的更多提示。
CatchBoundary
和 ErrorBoundary
/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
future: {
v2_errorBoundary: true,
},
};
在 v1 中,抛出的 Response
会渲染最近的 CatchBoundary
,而所有其他未处理的异常都会渲染 ErrorBoundary
。在 v2 中,没有 CatchBoundary
,所有未处理的异常都会渲染 ErrorBoundary
,无论是 response 还是其他异常。
此外,错误不再作为 props 传递给 ErrorBoundary
,而是通过 useRouteError
hook 访问。
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>
);
}
变为
import {
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
此 hook 现在称为 useNavigation
,以避免与最近的同名 React hook 混淆。它也不再具有 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;
}
import { 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
的 action 返回的数据可以获得您想要的用户体验。请随时在 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 导航并且只执行加载程序(而不是 action)。从功能上讲,它与 <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;
}
import { 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
上的 action 返回的数据可以获得您想要的用户体验。请随时在 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 请求并且只执行加载程序(而不是 action)。从功能上讲,它与 fetcher.load()
没有什么不同,只是用户可以通过输入指定搜索参数值。
在 v2 中,GET 提交更准确地反映为加载请求,因此会从 idle -> loading -> idle
,以使 fetcher.state
与普通 fetcher 加载的行为保持一致。如果您的 GET 提交来自 <fetcher.Form>
或 fetcher.submit()
,则会填充 fetcher.form*
,因此您可以根据需要进行区分。
imagesizes
和 imagesrcset
路由 links
属性都应该是 React camelCase 值,而不是 HTML 小写值。这两个值在 v1 中偷偷地变成了小写。在 v2 中,只有 camelCase 版本有效
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
。
/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
browserBuildDirectory: "./public/build",
};
/** @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
。
/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
devServerPort: 8002,
};
/** @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
,并指定模块路径,而不是目录。
/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
serverBuildDirectory: "./build",
};
/** @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();
Remix v2 也不再从 @remix-run/node
导出这些 polyfill 实现,而是你应该直接使用全局命名空间中的实例。这可能出现并需要更改的一个地方是你的 app/entry.server.tsx
文件,在那里你还需要将 Node 的 PassThrough
通过 createReadableStreamFromReadable
转换为 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
Source map 支持现在是应用服务器的责任。如果你正在使用 remix-serve
,则无需任何操作。如果你正在使用自己的应用服务器,则需要自己安装 source-map-support
。
npm i source-map-support
import sourceMapSupport from "source-map-support";
sourceMapSupport.install();
@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 模板。
@remix-run/vercel
运行时适配器已被弃用,转而使用开箱即用的 Vercel 功能,并且现在已从 Remix v2 中移除。请通过从你的 package.json
中移除 @remix-run/vercel
& @vercel/node
,移除你的 server.js
/server.ts
文件,并从你的 remix.config.js
中移除 server
& serverBuildPath
选项来更新你的代码。
由于移除了此适配器,我们还移除了我们的 Vercel 模板,转而使用 官方 Vercel 模板。
在 v2 中,如果你的项目中存在 PostCSS 和/或 Tailwind 配置文件,则这些工具会自动在 Remix 编译器中使用。
如果你在 Remix 之外有自定义的 PostCSS 和/或 Tailwind 设置,并且希望在迁移到 v2 时保持这些设置,则可以在你的 remix.config.js
中禁用这些功能。
/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
postcss: false,
tailwind: false,
};
"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
部分。