React Router v7 已發布。 查看文件
Vite
本頁面內容

Vite

Vite 是一個強大、高效能且可擴展的 JavaScript 專案開發環境。為了改進和擴展 Remix 的打包功能,我們現在支援 Vite 作為替代編譯器。未來,Vite 將成為 Remix 的預設編譯器。

傳統 Remix 編譯器與 Remix Vite

現有的 Remix 編譯器,透過 remix buildremix dev CLI 命令存取,並透過 remix.config.js 設定,現在稱為「傳統 Remix 編譯器」。

Remix Vite 外掛程式以及 remix vite:buildremix vite:dev CLI 命令統稱為「Remix Vite」。

未來,除非另有說明,否則文件將假設使用 Remix Vite。

開始使用

我們提供一些不同的基於 Vite 的範本,讓您開始使用。

# Minimal server:
npx create-remix@latest

# Express:
npx create-remix@latest --template remix-run/remix/templates/express

# Cloudflare:
npx create-remix@latest --template remix-run/remix/templates/cloudflare

# Cloudflare Workers:
npx create-remix@latest --template remix-run/remix/templates/cloudflare-workers

這些範本包含一個 vite.config.ts 檔案,用於設定 Remix Vite 外掛程式。

設定

Remix Vite 外掛程式透過專案根目錄中的 vite.config.ts 檔案進行設定。如需更多資訊,請參閱我們的 Vite 設定文件

Cloudflare

若要開始使用 Cloudflare,您可以使用 cloudflare 範本

npx create-remix@latest --template remix-run/remix/templates/cloudflare

有兩種方式可以在本機執行您的 Cloudflare 應用程式

# Vite
remix vite:dev

# Wrangler
remix vite:build # build app before running wrangler
wrangler pages dev ./build/client

雖然 Vite 提供更好的開發體驗,但 Wrangler 透過在 Cloudflare 的 workerd 執行階段而非 Node 中執行伺服器程式碼,提供更接近 Cloudflare 環境的模擬。

Cloudflare 代理

為了在 Vite 中模擬 Cloudflare 環境,Wrangler 提供 Node 代理至本機 workerd 繫結。Remix 的 Cloudflare 代理外掛程式會為您設定這些代理

import {
  vitePlugin as remix,
  cloudflareDevProxyVitePlugin as remixCloudflareDevProxy,
} from "@remix-run/dev";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [remixCloudflareDevProxy(), remix()],
});

然後,這些代理會出現在您 loaderaction 函式中的 context.cloudflare

export const loader = ({ context }: LoaderFunctionArgs) => {
  const { env, cf, ctx } = context.cloudflare;
  // ... more loader code here...
};

請參閱 Cloudflare 的 getPlatformProxy 文件,以取得有關每個代理的更多資訊。

繫結

若要設定 Cloudflare 資源的繫結

每當您變更 wrangler.toml 檔案時,您都需要執行 wrangler types 來重新產生您的繫結。

然後,您可以透過 context.cloudflare.env 存取您的繫結。例如,使用繫結為 MY_KVKV 命名空間

export async function loader({
  context,
}: LoaderFunctionArgs) {
  const { MY_KV } = context.cloudflare.env;
  const value = await MY_KV.get("my-key");
  return json({ value });
}

擴增載入內容

如果您想要將其他屬性新增至載入內容,您應該從共用模組匯出 getLoadContext 函式,以便 Vite、Wrangler 和 Cloudflare Pages 中的載入內容都以相同的方式擴增

import { type AppLoadContext } from "@remix-run/cloudflare";
import { type PlatformProxy } from "wrangler";

// When using `wrangler.toml` to configure bindings,
// `wrangler types` will generate types for those bindings
// into the global `Env` interface.
// Need this empty interface so that typechecking passes
// even if no `wrangler.toml` exists.
interface Env {}

type Cloudflare = Omit<PlatformProxy<Env>, "dispose">;

declare module "@remix-run/cloudflare" {
  interface AppLoadContext {
    cloudflare: Cloudflare;
    extra: string; // augmented
  }
}

type GetLoadContext = (args: {
  request: Request;
  context: { cloudflare: Cloudflare }; // load context _before_ augmentation
}) => AppLoadContext;

// Shared implementation compatible with Vite, Wrangler, and Cloudflare Pages
export const getLoadContext: GetLoadContext = ({
  context,
}) => {
  return {
    ...context,
    extra: "stuff",
  };
};

您必須將 getLoadContext 傳遞至 Cloudflare 代理外掛程式和 functions/[[path]].ts 中的請求處理常式,否則,根據您執行應用程式的方式,您將會得到不一致的載入內容擴增。

首先,在您的 Vite 設定中,將 getLoadContext 傳遞給 Cloudflare Proxy 外掛程式,以便在執行 Vite 時擴充載入內容。

import {
  vitePlugin as remix,
  cloudflareDevProxyVitePlugin as remixCloudflareDevProxy,
} from "@remix-run/dev";
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";

import { getLoadContext } from "./load-context";

export default defineConfig({
  plugins: [
    remixCloudflareDevProxy({ getLoadContext }),
    remix(),
  ],
});

接下來,在您的 functions/[[path]].ts 檔案中,將 getLoadContext 傳遞給請求處理常式,以便在執行 Wrangler 或部署到 Cloudflare Pages 時擴充載入內容。

import { createPagesFunctionHandler } from "@remix-run/cloudflare-pages";

// @ts-ignore - the server build file is generated by `remix vite:build`
import * as build from "../build/server";
import { getLoadContext } from "../load-context";

export const onRequest = createPagesFunctionHandler({
  build,
  getLoadContext,
});

拆分客戶端和伺服器程式碼

Vite 處理客戶端和伺服器程式碼的混合使用方式與傳統的 Remix 編譯器不同。如需更多資訊,請參閱我們關於拆分客戶端和伺服器程式碼的文件。

新的建置輸出路徑

Vite 管理 public 目錄的方式與現有的 Remix 編譯器有明顯差異。Vite 會將 public 目錄中的檔案複製到客戶端建置目錄中,而 Remix 編譯器則保持 public 目錄不變,並使用子目錄 (public/build) 作為客戶端建置目錄。

為了使預設的 Remix 專案結構與 Vite 的運作方式保持一致,已變更了建置輸出路徑。現在只有一個 buildDirectory 選項,預設為 "build",取代了個別的 assetsBuildDirectoryserverBuildDirectory 選項。這表示,預設情況下,伺服器現在會編譯到 build/server 中,而客戶端則會編譯到 build/client 中。

這也表示已變更下列組態預設值

  • publicPath 已被 Vite 的 "base" 選項取代,後者的預設值為 "/" 而非 "/build/"
  • serverBuildPath 已被 serverBuildFile 取代,後者的預設值為 "index.js"。此檔案將會寫入您設定的 buildDirectory 中的伺服器目錄。

Remix 轉向 Vite 的原因之一是為了讓您在採用 Remix 時減少需要學習的內容。這表示,對於您想使用的任何其他捆綁功能,您應該參考 Vite 文件Vite 外掛程式社群,而不是 Remix 文件。

Vite 具有許多 功能外掛程式,這些功能並未內建於現有的 Remix 編譯器中。使用任何此類功能將會導致現有的 Remix 編譯器無法編譯您的應用程式,因此,如果您打算從此以後只使用 Vite,則只能使用它們。

遷移

設定 Vite

👉 將 Vite 安裝為開發相依性

npm install -D vite

Remix 現在只是一個 Vite 外掛程式,因此您需要將它連接到 Vite。

👉 將您 Remix 應用程式根目錄中的 remix.config.js 取代為 vite.config.ts

import { vitePlugin as remix } from "@remix-run/dev";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [remix()],
});

應將支援的 Remix 組態選項的子集直接傳遞給外掛程式

export default defineConfig({
  plugins: [
    remix({
      ignoredRouteFiles: ["**/*.css"],
    }),
  ],
});

HMR & HDR

Vite 為開發功能 (如 HMR) 提供了一個強大的客戶端執行階段,使得 <LiveReload /> 元件過時。在開發中使用 Remix Vite 外掛程式時,<Scripts /> 元件會自動包含 Vite 的客戶端執行階段和其他僅限開發的指令碼。

👉 移除 <LiveReload/>,保留 <Scripts />

  import {
-   LiveReload,
    Outlet,
    Scripts,
  }

  export default function App() {
    return (
      <html>
        <head>
        </head>
        <body>
          <Outlet />
-         <LiveReload />
          <Scripts />
        </body>
      </html>
    )
  }

TypeScript 整合

Vite 會處理各種不同檔案類型的匯入,有時其方式與現有的 Remix 編譯器不同,因此讓我們從 vite/client 中參考 Vite 的類型,而不是從 @remix-run/dev 中參考過時的類型。

由於 vite/client 提供的模組類型與 @remix-run/dev 隱式包含的模組類型不相容,您還需要啟用 TypeScript 設定中的 skipLibCheck 旗標。一旦 Vite 外掛程式成為預設編譯器,Remix 將不再需要此旗標。

👉 更新 tsconfig.json

更新 tsconfig.json 中的 types 欄位,並確保正確設定 skipLibCheckmodulemoduleResolution

{
  "compilerOptions": {
    "types": ["@remix-run/node", "vite/client"],
    "skipLibCheck": true,
    "module": "ESNext",
    "moduleResolution": "Bundler"
  }
}

👉 更新/移除 remix.env.d.ts

移除 remix.env.d.ts 中的下列類型宣告

- /// <reference types="@remix-run/dev" />
- /// <reference types="@remix-run/node" />

如果 remix.env.d.ts 現在為空,請將其刪除

rm remix.env.d.ts

從 Remix App Server 遷移

如果您在開發中使用 remix-serve (或在沒有 -c 旗標的情況下使用 remix dev),您需要切換到新的最小開發伺服器。它內建於 Remix Vite 外掛程式中,並會在您執行 remix vite:dev 時接管。

Remix Vite 外掛程式不會安裝任何全域 Node polyfill,因此如果您依賴 remix-serve 來提供它們,則需要自行安裝它們。最簡單的方法是在您的 Vite 設定頂端呼叫 installGlobals

Vite 開發伺服器的預設連接埠與 remix-serve 不同,因此如果您想維持相同的連接埠,則需要透過 Vite 的 server.port 選項進行設定。

您還需要更新到新的建置輸出路徑,伺服器的路徑為 build/server,而客戶端資源的路徑為 build/client

👉 更新您的 devbuildstart 指令碼

{
  "scripts": {
    "dev": "remix vite:dev",
    "build": "remix vite:build",
    "start": "remix-serve ./build/server/index.js"
  }
}

👉 在您的 Vite 設定中安裝全域 Node polyfill

import { vitePlugin as remix } from "@remix-run/dev";
+import { installGlobals } from "@remix-run/node";
import { defineConfig } from "vite";

+installGlobals();

export default defineConfig({
  plugins: [remix()],
});

👉 設定您的 Vite 開發伺服器連接埠 (選用)

export default defineConfig({
  server: {
    port: 3000,
  },
  plugins: [remix()],
});

遷移自訂伺服器

如果您在開發中使用自訂伺服器,則需要編輯您的自訂伺服器以使用 Vite 的 connect 中介軟體。這會在開發期間將資源請求和初始轉譯請求委派給 Vite,讓您即使使用自訂伺服器也能受益於 Vite 出色的 DX。

然後,您可以在開發期間載入名為 "virtual:remix/server-build" 的虛擬模組,以建立基於 Vite 的請求處理常式。

您還需要更新您的伺服器程式碼,以參考新的建置輸出路徑,伺服器建置的路徑為 build/server,而客戶端資源的路徑為 build/client

例如,如果您使用 Express,以下說明如何操作。

👉 更新您的 server.mjs 檔案

import { createRequestHandler } from "@remix-run/express";
import { installGlobals } from "@remix-run/node";
import express from "express";

installGlobals();

const viteDevServer =
  process.env.NODE_ENV === "production"
    ? undefined
    : await import("vite").then((vite) =>
        vite.createServer({
          server: { middlewareMode: true },
        })
      );

const app = express();

// handle asset requests
if (viteDevServer) {
  app.use(viteDevServer.middlewares);
} else {
  app.use(
    "/assets",
    express.static("build/client/assets", {
      immutable: true,
      maxAge: "1y",
    })
  );
}
app.use(express.static("build/client", { maxAge: "1h" }));

// handle SSR requests
app.all(
  "*",
  createRequestHandler({
    build: viteDevServer
      ? () =>
          viteDevServer.ssrLoadModule(
            "virtual:remix/server-build"
          )
      : await import("./build/server/index.js"),
  })
);

const port = 3000;
app.listen(port, () =>
  console.log("https://127.0.0.1:" + port)
);

👉 更新您的 builddevstart 指令碼

{
  "scripts": {
    "dev": "node ./server.mjs",
    "build": "remix vite:build",
    "start": "cross-env NODE_ENV=production node ./server.mjs"
  }
}

如果您願意,也可以使用 TypeScript 來撰寫您的自訂伺服器。然後,您可以使用 tsxtsm 等工具來執行您的自訂伺服器

tsx ./server.ts
node --loader tsm ./server.ts

請記住,如果您這樣做,伺服器初始啟動可能會有一些明顯的延遲。

遷移 Cloudflare Functions

Remix Vite 外掛程式僅官方支援 Cloudflare Pages,它是專為全端應用程式而設計的,不像 Cloudflare Workers Sites。如果您目前使用 Cloudflare Workers Sites,請參閱Cloudflare Pages 遷移指南

👉 將 cloudflareDevProxyVitePlugin 新增到 remix 外掛程式之前,以正確覆寫 vite 開發伺服器的中介軟體!

import {
  vitePlugin as remix,
  cloudflareDevProxyVitePlugin,
} from "@remix-run/dev";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [cloudflareDevProxyVitePlugin(), remix()],
});

您的 Cloudflare 應用程式可能正在設定 Remix 組態的 server 欄位,以產生一個萬用 Cloudflare Function。使用 Vite 時,不再需要這種間接性。相反地,您可以直接為 Cloudflare 撰寫萬用路由,就像您為 Express 或任何其他自訂伺服器撰寫路由一樣。

👉 為 Remix 建立萬用路由

import { createPagesFunctionHandler } from "@remix-run/cloudflare-pages";

// @ts-ignore - the server build file is generated by `remix vite:build`
import * as build from "../build/server";

export const onRequest = createPagesFunctionHandler({
  build,
});

👉 透過 context.cloudflare.env 而不是 context.env 來存取繫結和環境變數

雖然您主要會在開發期間使用 Vite,但您也可以使用 Wrangler 來預覽和部署您的應用程式。

如需更多資訊,請參閱此文件的Cloudflare章節。

👉 更新您的 package.json 指令碼

{
  "scripts": {
    "dev": "remix vite:dev",
    "build": "remix vite:build",
    "preview": "wrangler pages dev ./build/client",
    "deploy": "wrangler pages deploy ./build/client"
  }
}

遷移對建置輸出路徑的參考

在使用現有 Remix 編譯器的預設選項時,伺服器會編譯到 build 中,而客戶端會編譯到 public/build 中。由於 Vite 通常處理其 public 目錄的方式與現有 Remix 編譯器不同,因此這些輸出路徑已變更。

👉 更新對建置輸出路徑的參考

  • 預設情況下,伺服器現在會編譯到 build/server 中。
  • 預設情況下,客戶端現在會編譯到 build/client 中。

例如,若要從 Blues Stack 更新 Dockerfile

-COPY --from=build /myapp/build /myapp/build
-COPY --from=build /myapp/public /myapp/public
+COPY --from=build /myapp/build/server /myapp/build/server
+COPY --from=build /myapp/build/client /myapp/build/client

設定路徑別名

Remix 編譯器會利用 tsconfig.json 中的 paths 選項來解析路徑別名。這通常在 Remix 社群中用於將 ~ 定義為 app 目錄的別名。

Vite 預設不提供任何路徑別名。如果您依賴此功能,則可以安裝 vite-tsconfig-paths 外掛程式,以自動從 Vite 中的 tsconfig.json 解析路徑別名,使其與 Remix 編譯器的行為一致。

👉 安裝 vite-tsconfig-paths

npm install -D vite-tsconfig-paths

👉 vite-tsconfig-paths 新增到您的 Vite 設定中

import { vitePlugin as remix } from "@remix-run/dev";
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";

export default defineConfig({
  plugins: [remix(), tsconfigPaths()],
});

移除 @remix-run/css-bundle

Vite 內建支援 CSS 副作用匯入、PostCSS 和 CSS 模組,以及其他 CSS 捆綁功能。Remix Vite 外掛程式會自動將捆綁的 CSS 附加到相關的路由。

@remix-run/css-bundle封裝在搭配使用 Vite 時是多餘的,因為其 cssBundleHref 匯出永遠會是 undefined

👉 解除安裝 @remix-run/css-bundle

npm uninstall @remix-run/css-bundle

👉 移除對 cssBundleHref 的參考

- import { cssBundleHref } from "@remix-run/css-bundle";
  import type { LinksFunction } from "@remix-run/node"; // or cloudflare/deno

  export const links: LinksFunction = () => [
-   ...(cssBundleHref
-     ? [{ rel: "stylesheet", href: cssBundleHref }]
-     : []),
    // ...
  ];

如果路由的 links 函式僅用於連接 cssBundleHref,您可以將其完全移除。

- import { cssBundleHref } from "@remix-run/css-bundle";
- import type { LinksFunction } from "@remix-run/node"; // or cloudflare/deno

- export const links: LinksFunction = () => [
-   ...(cssBundleHref
-     ? [{ rel: "stylesheet", href: cssBundleHref }]
-     : []),
- ];

這對於其他形式的 CSS 打包並非必要,例如 CSS Modules、CSS side effect imports、Vanilla Extract 等。

如果您在 links 函式中引用 CSS,您需要更新對應的 CSS 導入,以使用 Vite 的明確 ?url 導入語法。

👉 ?url 新增至 links 中使用的 CSS 導入

.css?url 導入需要 Vite v5.1 或更新版本

-import styles from "~/styles/dashboard.css";
+import styles from "~/styles/dashboard.css?url";

export const links = () => {
  return [
    { rel: "stylesheet", href: styles }
  ];
}

透過 PostCSS 啟用 Tailwind

如果您的專案使用 Tailwind CSS,您首先需要確保您有一個 PostCSS 設定檔,該檔案將會被 Vite 自動選取。這是因為當 Remix 的 tailwind 選項啟用時,Remix 編譯器不需要 PostCSS 設定檔。

👉 如果缺少 PostCSS 設定檔,請新增,包括 tailwindcss 外掛

export default {
  plugins: {
    tailwindcss: {},
  },
};

如果您的專案已經有 PostCSS 設定檔,您需要新增 tailwindcss 外掛(如果尚未存在)。這是因為當 Remix 的 tailwind 設定選項啟用時,Remix 編譯器會自動包含此外掛。

👉 如果缺少 tailwindcss 外掛,請將其新增至您的 PostCSS 設定檔

export default {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
};

👉 遷移 Tailwind CSS 導入

如果您在 links 函式中引用您的 Tailwind CSS 檔案,您需要遷移您的 Tailwind CSS 導入語句。

新增 Vanilla Extract 外掛

如果您使用 Vanilla Extract,您需要設定 Vite 外掛。

👉 安裝官方 適用於 Vite 的 Vanilla Extract 外掛

npm install -D @vanilla-extract/vite-plugin

👉 將 Vanilla Extract 外掛新增至您的 Vite 設定

import { vitePlugin as remix } from "@remix-run/dev";
import { vanillaExtractPlugin } from "@vanilla-extract/vite-plugin";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [remix(), vanillaExtractPlugin()],
});

新增 MDX 外掛

如果您使用 MDX,由於 Vite 的外掛 API 是 Rollup 外掛 API 的擴展,您應該使用官方的 MDX Rollup 外掛

👉 安裝 MDX Rollup 外掛

npm install -D @mdx-js/rollup

Remix 外掛預期會處理 JavaScript 或 TypeScript 檔案,因此來自其他語言(如 MDX)的任何轉譯都必須先完成。在這種情況下,這表示將 MDX 外掛置於 Remix 外掛之前

👉 將 MDX Rollup 外掛新增至您的 Vite 設定

import mdx from "@mdx-js/rollup";
import { vitePlugin as remix } from "@remix-run/dev";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [mdx(), remix()],
});
新增 MDX frontmatter 支援

Remix 編譯器允許您在 MDX 中定義 frontmatter。如果您正在使用此功能,您可以使用 remark-mdx-frontmatter 在 Vite 中實現此功能。

👉 安裝所需的 Remark frontmatter 外掛

npm install -D remark-frontmatter remark-mdx-frontmatter

👉 將 Remark frontmatter 外掛傳遞給 MDX Rollup 外掛

import mdx from "@mdx-js/rollup";
import { vitePlugin as remix } from "@remix-run/dev";
import remarkFrontmatter from "remark-frontmatter";
import remarkMdxFrontmatter from "remark-mdx-frontmatter";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [
    mdx({
      remarkPlugins: [
        remarkFrontmatter,
        remarkMdxFrontmatter,
      ],
    }),
    remix(),
  ],
});

在 Remix 編譯器中,frontmatter 匯出被命名為 attributes。這與 frontmatter 外掛的預設匯出名稱 frontmatter 不同。儘管可以設定 frontmatter 匯出名稱,我們建議您更新您的應用程式程式碼以使用預設匯出名稱。

👉 將 MDX 檔案中的 attributes 匯出重新命名為 frontmatter

  ---
  title: Hello, World!
  ---

- # {attributes.title}
+ # {frontmatter.title}

👉 將消費者的 MDX attributes 匯出重新命名為 frontmatter

  import Component, {
-   attributes,
+   frontmatter,
  } from "./posts/first-post.mdx";
定義 MDX 檔案的類型

👉 *.mdx 檔案的類型新增至 env.d.ts

/// <reference types="@remix-run/node" />
/// <reference types="vite/client" />

declare module "*.mdx" {
  let MDXComponent: (props: any) => JSX.Element;
  export const frontmatter: any;
  export default MDXComponent;
}
將 MDX frontmatter 對應至路由匯出

Remix 編譯器允許您在 frontmatter 中定義 headersmetahandle 路由匯出。這個 Remix 特有的功能顯然不受 remark-mdx-frontmatter 外掛支援。如果您正在使用此功能,您應該自行手動將 frontmatter 對應至路由匯出

👉 將 MDX 路由的 frontmatter 對應至路由匯出

---
meta:
  - title: My First Post
  - name: description
    content: Isn't this awesome?
headers:
  Cache-Control: no-cache
---

export const meta = frontmatter.meta;
export const headers = frontmatter.headers;

# Hello World

請注意,由於您明確地對應 MDX 路由匯出,您現在可以自由使用您喜歡的任何 frontmatter 結構。

---
title: My First Post
description: Isn't this awesome?
---

export const meta = () => {
  return [
    { title: frontmatter.title },
    {
      name: "description",
      content: frontmatter.description,
    },
  ];
};

# Hello World
更新 MDX 檔案名稱的使用方式

Remix 編譯器還從所有 MDX 檔案提供 filename 匯出。這主要是為了啟用連結至 MDX 路由的集合。如果您正在使用此功能,您可以使用 glob 導入在 Vite 中實現此功能,這會為您提供一個方便的資料結構,將檔案名稱對應至模組。這使得維護 MDX 檔案的列表變得更加容易,因為您不再需要手動導入每個檔案。

例如,要導入 posts 目錄中的所有 MDX 檔案

const posts = import.meta.glob("./posts/*.mdx");

這相當於手動編寫此程式碼

const posts = {
  "./posts/a.mdx": () => import("./posts/a.mdx"),
  "./posts/b.mdx": () => import("./posts/b.mdx"),
  "./posts/c.mdx": () => import("./posts/c.mdx"),
  // etc.
};

如果您願意,也可以積極地導入所有 MDX 檔案

const posts = import.meta.glob("./posts/*.mdx", {
  eager: true,
});

除錯

您可以使用 NODE_OPTIONS 環境變數 來啟動除錯工作階段

NODE_OPTIONS="--inspect-brk" npm run dev

然後您可以從瀏覽器附加除錯器。例如,在 Chrome 中,您可以開啟 chrome://inspect 或點擊開發人員工具中的 NodeJS 圖示來附加除錯器。

vite-plugin-inspect

vite-plugin-inspect 會顯示每個 Vite 外掛程式如何轉換您的程式碼,以及每個外掛程式所花費的時間。

效能

Remix 包含一個 --profile 旗標,用於效能分析。

remix vite:build --profile

當使用 --profile 執行時,將會產生一個 .cpuprofile 檔案,該檔案可以共享或上傳到 speedscope.app 以進行分析。

您也可以在開發期間透過在開發伺服器執行時按下 p + enter 來啟動新的分析工作階段或停止目前的工作階段。如果您需要分析開發伺服器的啟動,您也可以使用 --profile 旗標在啟動時初始化分析工作階段

remix vite:dev --profile

請記住,您隨時可以查看 Vite 效能文件以取得更多提示!

捆綁分析

為了視覺化和分析您的捆綁包,您可以使用 rollup-plugin-visualizer 外掛

import { vitePlugin as remix } from "@remix-run/dev";
import { visualizer } from "rollup-plugin-visualizer";

export default defineConfig({
  plugins: [
    remix(),
    // `emitFile` is necessary since Remix builds more than one bundle!
    visualizer({ emitFile: true }),
  ],
});

然後當您執行 remix vite:build 時,它會在您的每個捆綁包中產生一個 stats.html 檔案

build
├── client
│   ├── assets/
│   ├── favicon.ico
│   └── stats.html 👈
└── server
    ├── index.js
    └── stats.html 👈

在您的瀏覽器中開啟 stats.html 以分析您的捆綁包。

疑難排解

查看 除錯效能 區段以取得一般的疑難排解提示。此外,請查看是否有人遇到類似的問題,方法是瀏覽 github 上 remix vite 外掛的已知問題

HMR

如果您期望熱更新但卻收到整頁重新載入,請查看我們關於熱模組替換的討論,以了解更多關於 React Fast Refresh 的限制以及常見問題的解決方案。

ESM / CJS

Vite 支援 ESM 和 CJS 依賴項,但有時您可能仍然會遇到 ESM / CJS 互通性的問題。通常,這是因為依賴項未正確設定為支援 ESM。我們不會責怪他們,因為正確地支援 ESM 和 CJS 真的非常棘手

如需修正範例錯誤的逐步說明,請查看 🎥 如何在 Remix 中修正 CJS/ESM 錯誤

若要診斷您的某個依賴項是否設定錯誤,請查看 publint類型是否錯誤。此外,您可以使用 vite-plugin-cjs-interop 外掛來解決外部 CJS 依賴項的 default 匯出問題。

最後,您也可以明確設定哪些依賴項要與 Vite 的 ssr.noExternal 選項捆綁到伺服器捆綁包中,以模擬 Remix 編譯器的 serverDependenciesToBundle 與 Remix Vite 外掛。

開發期間瀏覽器中的伺服器程式碼錯誤

如果您在開發期間看到瀏覽器主控台中指向伺服器程式碼的錯誤,您可能需要明確隔離僅限伺服器程式碼。例如,如果您看到類似以下的內容

Uncaught ReferenceError: process is not defined

那麼您需要追蹤哪個模組正在提取需要伺服器專用全域變數(如 process)的依賴項,並將程式碼隔離在單獨的 .server 模組中或使用 vite-env-only。由於 Vite 使用 Rollup 在生產環境中進行 tree shaking,因此這些錯誤只會在開發期間發生。

將外掛程式與其他基於 Vite 的工具 (例如 Vitest、Storybook) 搭配使用

Remix Vite 外掛程式僅適用於應用程式的開發伺服器和生產建置。雖然還有其他基於 Vite 的工具(如 Vitest 和 Storybook)會使用 Vite 設定檔,但 Remix Vite 外掛程式並非設計為與這些工具一起使用。目前,我們建議在使用其他基於 Vite 的工具時排除此外掛程式。

適用於 Vitest

import { vitePlugin as remix } from "@remix-run/dev";
import { defineConfig, loadEnv } from "vite";

export default defineConfig({
  plugins: [!process.env.VITEST && remix()],
  test: {
    environment: "happy-dom",
    // Additionally, this is to load ".env.test" during vitest
    env: loadEnv("test", process.cwd(), ""),
  },
});

適用於 Storybook

import { vitePlugin as remix } from "@remix-run/dev";
import { defineConfig } from "vite";

const isStorybook = process.argv[1]?.includes("storybook");

export default defineConfig({
  plugins: [!isStorybook && remix()],
});

或者,您可以為每個工具使用單獨的 Vite 設定檔。例如,要使用專門針對 Remix 的 Vite 設定

remix vite:dev --config vite.config.remix.ts

當不提供 Remix Vite 外掛時,您的設定可能還需要提供 Vite Plugin React。例如,當使用 Vitest 時

import { vitePlugin as remix } from "@remix-run/dev";
import react from "@vitejs/plugin-react";
import { defineConfig, loadEnv } from "vite";

export default defineConfig({
  plugins: [!process.env.VITEST ? remix() : react()],
  test: {
    environment: "happy-dom",
    // Additionally, this is to load ".env.test" during vitest
    env: loadEnv("test", process.cwd(), ""),
  },
});

當文件重新掛載時,樣式在開發中消失

當使用 React 渲染整個文件時(如 Remix 所做的那樣),您可能會遇到動態將元素注入 head 元素時的問題。如果文件重新掛載,則現有的 head 元素會被移除並替換為全新的元素,從而移除 Vite 在開發期間注入的任何 style 元素。

這是 React 中已知的問題,已在其 Canary 版本通道中修復。如果您了解其中涉及的風險,您可以將您的應用程式鎖定到特定的 React 版本,然後使用 套件覆寫 來確保這是整個專案中使用的唯一 React 版本。例如

{
  "dependencies": {
    "react": "18.3.0-canary-...",
    "react-dom": "18.3.0-canary-..."
  },
  "overrides": {
    "react": "18.3.0-canary-...",
    "react-dom": "18.3.0-canary-..."
  }
}

作為參考,這就是 Next.js 在內部代表您處理 React 版本的方式,因此這種方法比您想像的更廣泛使用,即使它不是 Remix 作為預設提供的功能。

值得強調的是,Vite 注入的樣式問題僅在開發中發生。生產版本不會有此問題,因為會產生靜態 CSS 檔案。

在 Remix 中,當渲染在您的 根路由的預設組件匯出及其 ErrorBoundary 和/或 HydrateFallback 匯出之間交替時,可能會出現此問題,因為這會導致掛載新的文件級別組件。

這也可能因水合錯誤而發生,因為這會導致 React 從頭開始重新渲染整個頁面。水合錯誤可能是由您的應用程式程式碼引起的,但也可能是由操作文件的瀏覽器擴充功能引起的。

這與 Vite 相關,因為在開發過程中,Vite 會將 CSS 導入轉換為 JS 檔案,這些檔案會將其樣式作為副作用注入到文件中。Vite 這樣做是為了支援靜態 CSS 檔案的延遲載入和 HMR。

例如,假設您的應用程式具有以下 CSS 檔案

* { margin: 0 }

在開發過程中,當作為副作用導入時,此 CSS 檔案將被轉換為以下 JavaScript 程式碼

import {createHotContext as __vite__createHotContext} from "/@vite/client";
import.meta.hot = __vite__createHotContext("/app/styles.css");
import {updateStyle as __vite__updateStyle, removeStyle as __vite__removeStyle} from "/@vite/client";
const __vite__id = "/path/to/app/styles.css";
const __vite__css = "*{margin:0}"
__vite__updateStyle(__vite__id, __vite__css);
import.meta.hot.accept();
import.meta.hot.prune(()=>__vite__removeStyle(__vite__id));

此轉換不適用於生產程式碼,這就是為什麼此樣式問題僅影響開發的原因。

開發中的 Wrangler 錯誤

當使用 Cloudflare Pages 時,您可能會從 wrangler pages dev 遇到以下錯誤

ERROR: Your worker called response.clone(), but did not read the body of both clones.
This is wasteful, as it forces the system to buffer the entire response body
in memory, rather than streaming it through. This may cause your worker to be
unexpectedly terminated for going over the memory limit. If you only meant to
copy the response headers and metadata (e.g. in order to be able to modify
them), use `new Response(response.body, response)` instead.

這是 Wrangler 的已知問題

致謝

Vite 是一個很棒的專案,我們感謝 Vite 團隊所做的工作。特別感謝來自 Vite 團隊的 Matias Capeletto、Arnaud Barré 和 Bjorn Lu 的指導。

Remix 社群很快就探索了 Vite 的支援,我們感謝他們的貢獻

最後,我們受到其他框架如何實作 Vite 支援的啟發

文件和範例的授權條款為 MIT