React Router v7 已發布。 查看文件
升級至 v2
本頁內容

升級到 v2

本文件提供使用 Classic Remix 編譯器 從 v1 遷移到 v2 的指南。有關遷移到 Vite 的其他指南,請參閱 Remix Vite 文件

所有 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 整合的範例,或按照以下步驟操作

  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 編譯器發送「準備就緒」訊息

    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 返回一個描述符陣列並自行管理合併,而不是從 meta 返回一個物件。這使得 meta API 更接近 links,並且允許更靈活地控制 meta 標籤的呈現方式。

此外,<Meta /> 將不再為階層中的每個路由呈現 meta。只會呈現葉路由中從 meta 返回的資料。您仍然可以通過存取函數參數中的matches來包含來自父路由的 meta。

如需更多關於此變更的背景資訊,請參閱 原始的 v2 meta 提案

在 v2 中使用 v1 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 參數存取您所有的路由匹配,其中包含每個匹配的 loader 資料。

為了複製 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 的提示。

CatchBoundaryErrorBoundary

/** @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.statenavigation.formData 或從具有 useActionData 的 action 返回的資料,即可獲得您想要的 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 導航,並且只執行 loaders(而不是 actions)。在功能上,它與 <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 上的 action 返回的資料,即可獲得您想要的 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 請求,並且只執行 loader(而不是 action)。在功能上,它與 fetcher.load() 沒有區別,只是使用者可能會透過輸入指定搜尋參數值。

在 v2 中,GET 提交更準確地反映為載入請求,因此會從 idle -> loading -> idle 變化,以使 fetcher.state 與一般 fetcher 載入的行為保持一致。如果您的 GET 提交來自 <fetcher.Form>fetcher.submit(),則會填入 fetcher.form*,因此您可以根據需要進行區分。

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

移除匯出的 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

Source map 的支援現在是應用程式伺服器的責任。如果您正在使用 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 編譯器中自動使用。

如果您在遷移到 v2 時想要保留 Remix 外部的自訂 PostCSS 和/或 Tailwind 設定,則可以在您的 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 授權