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 編譯器發送「準備就緒」訊息
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
返回一個描述符陣列並自行管理合併,而不是從 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
參數存取您所有的路由匹配,其中包含每個匹配的 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 的提示。
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 返回的資料,即可獲得您想要的 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.state
、fetcher.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*
,因此您可以根據需要進行區分。
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
轉換為 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();
@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 編譯器中自動使用。
如果您在遷移到 v2 時想要保留 Remix 外部的自訂 PostCSS 和/或 Tailwind 設定,則可以在您的 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
章節。