升级到 v2
本页内容

升级到 v2

本文档提供了在使用 经典 Remix 编译器 时从 v1 迁移到 v2 的指南。有关迁移到 Vite 的更多指南,请参阅 Remix Vite 文档

所有 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 集成的示例,或按照以下步骤操作

  1. 启用 v2_dev

    /** @type {import('@remix-run/dev').AppConfig} */
    module.exports = {
      future: {
        v2_dev: true,
      },
    };
    
  2. 更新 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'",
       }
     }
    
  3. 在您的应用运行后,向 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));
    });
    
  4. (可选) --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);
  },
};

升级到新的约定

  • 路由嵌套现在通过文件名中的点 (.) 创建,而不是文件夹嵌套。
  • 段中的后缀下划线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

请注意,父路由现在组合在一起,而不是在它们之间有数十个路由(如身份验证路由)。具有相同路径但嵌套不同的路由(如dashboarddashboard_)也会组合在一起。

使用新的约定,任何路由都可以是包含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": "...",
  };
}
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",
      },
    },
  ];
}

有关合并路由元数据的更多提示,请参阅元数据文档。

CatchBoundaryErrorBoundary

/** @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.statenavigation.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.statefetcher.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*将填充,因此您可以根据需要进行区分。

路由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();

移除导出的 polyfill

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();

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 中删除 serverserverBuildPath 选项来更新您的代码。

由于移除了此适配器,我们还移除了我们的 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 部分。

文档和示例根据以下许可证授权 MIT