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 中使用 未来特性。可以逐一启用它们以避免项目开发中断。启用所有特性后,升级到 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(`👉 https://127.0.0.1:${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
API 更接近于links
,并且它允许更灵活地控制如何呈现元标记。
此外,<Meta />
将不再为层次结构中的每个路由呈现元数据。只会呈现叶路由中meta
返回的数据。您仍然可以选择通过访问函数参数中的matches
来包含父路由的元数据。
有关此更改的更多背景信息,请参阅原始 v2 meta
提案。
meta
约定您可以使用@remix-run/v1-meta
包更新您的meta
导出以继续使用 v1 约定。
使用metaV1
函数,您可以传入meta
函数的参数以及它当前返回的相同对象。此函数将使用相同的合并逻辑将叶路由的元数据与其**直接父路由**的元数据合并,然后再将其转换为 v2 中可用的元数据描述符数组。
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": "...",
});
}
需要注意的是,此函数不会默认跨整个层次结构合并元数据。这是因为您可能有一些路由直接返回对象数组,而没有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"];
}
变成
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",
},
},
];
}
有关合并路由元数据的更多提示,请参阅元数据文档。
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>
);
}
变成
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
此钩子现在称为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;
}
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
从操作返回的数据可以获得您想要的 UX。随时在 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;
}
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
上从操作返回的数据可以获得您想要的 UX。随时在 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
。
/** @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,
},
},
};
尽管我们建议明确指定浏览器 bundle 中允许的 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
转换为 Web ReadableStream
,方法是通过 createReadableStreamFromReadable
。
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();
@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
部分。