Vite
On this page

Vite

Vite 是一个功能强大、性能卓越且可扩展的 JavaScript 项目开发环境。为了改进和扩展 Remix 的打包功能,我们现在支持 Vite 作为替代编译器。将来,Vite 将成为 Remix 的默认编译器。

¥Vite is a powerful, performant, and extensible development environment for JavaScript projects. To improve and extend Remix's bundling capabilities, we now support Vite as an alternative compiler. In the future, Vite will become the default compiler for Remix.

Classic Remix 编译器 vs. Remix Vite

¥Classic Remix Compiler vs. Remix Vite

现有的 Remix 编译器(可通过 remix buildremix dev CLI 命令访问,并通过 remix.config.js 配置)现在称为 "Classic Remix 编译器"。

¥The existing Remix compiler, accessed via the remix build and remix dev CLI commands and configured via remix.config.js, is now referred to as the "Classic Remix Compiler".

Remix Vite 插件以及 remix vite:buildremix vite:dev CLI 命令统称为 "Remix Vite"。

¥The Remix Vite plugin and the remix vite:build and remix vite:dev CLI commands are collectively referred to as "Remix Vite".

除非另有说明,否则后续文档将假定使用 Remix Vite。

¥Moving forwards, documentation will assume usage of Remix Vite unless otherwise stated.

入门指南

¥Getting started

我们提供了一些不同的基于 Vite 的模板来帮助你入门。

¥We've got a few different Vite-based templates to get you started.

# 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 插件就是在其中配置的。

¥These templates include a vite.config.ts file which is where the Remix Vite plugin is configured.

配置

¥Configuration

Remix Vite 插件通过项目根目录下的 vite.config.ts 文件进行配置。有关更多信息,请参阅我们的 Vite 配置文档

¥The Remix Vite plugin is configured via a vite.config.ts file at the root of your project. For more information, see our Vite config documentation.

Cloudflare

要开始使用 Cloudflare,你可以使用 cloudflare 模板:

¥To get started with Cloudflare, you can use the cloudflare template:

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

在本地运行 Cloudflare 应用有两种方式:

¥There are two ways to run your Cloudflare app locally:

# Vite
remix vite:dev

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

虽然 Vite 提供了更好的开发体验,但 Wrangler 通过在 Cloudflare 的 workerd 运行时 而不是 Node 中运行服务器代码,提供了更接近 Cloudflare 环境的模拟。

¥While Vite provides a better development experience, Wrangler provides closer emulation of the Cloudflare environment by running your server code in Cloudflare's workerd runtime instead of Node.

Cloudflare 代理

¥Cloudflare Proxy

为了在 Vite 中模拟 Cloudflare 环境,Wrangler 提供了 Node 代理到本地 workerd 绑定。Remix 的 Cloudflare Proxy 插件可为你设置以下代理:

¥To simulate the Cloudflare environment in Vite, Wrangler provides Node proxies to local workerd bindings. Remix's Cloudflare Proxy plugin sets up these proxies for you:

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

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

然后,代理即可在 context.cloudflare 中的 loaderaction 函数中使用:

¥The proxies are then available within context.cloudflare in your loader or action functions:

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

查看 Cloudflare 的 getPlatformProxy 文档 以获取有关每个代理的更多信息。

¥Check out Cloudflare's getPlatformProxy docs for more information on each of these proxies.

绑定

¥Bindings

要配置 Cloudflare 资源的绑定:

¥To configure bindings for Cloudflare resources:

每当你更改 wrangler.toml 文件时,你都需要运行 wrangler types 来重新生成绑定。

¥Whenever you change your wrangler.toml file, you'll need to run wrangler types to regenerate your bindings.

然后,你可以通过 context.cloudflare.env 访问你的绑定。例如,将 KV 命名空间 绑定为 MY_KV

¥Then, you can access your bindings via context.cloudflare.env. For example, with a KV namespace bound as MY_KV:

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

增强加载上下文

¥Augmenting load context

如果你想向加载上下文添加其他属性,你应该从共享模块导出 getLoadContext 函数,以便 Vite、Wrangler 和 Cloudflare Pages 中的加载上下文都以相同的方式进行增强:

¥If you'd like to add additional properties to the load context, you should export a getLoadContext function from a shared module so that load context in Vite, Wrangler, and Cloudflare Pages are all augmented in the same way:

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",
  };
};

你必须将 Cloudflare 代理插件和 functions/[[path]].ts 中的请求处理程序传入 getLoadContextboth,否则你将根据应用的运行方式获得不一致的加载上下文增强。

首先,将 getLoadContext 传递给 Vite 配置中的 Cloudflare Proxy 插件,以在运行 Vite 时增强加载上下文:

¥First, pass in getLoadContext to the Cloudflare Proxy plugin in your Vite config to augment load context when running 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(),
  ],
});

接下来,将 getLoadContext 传递给 functions/[[path]].ts 文件中的请求处理程序,以便在运行 Wrangler 或部署到 Cloudflare Pages 时增强加载上下文:

¥Next, pass in getLoadContext to the request handler in your functions/[[path]].ts file to augment load context when running Wrangler or when deploying to 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,
});

拆分客户端和服务端代码

¥Splitting up client and server code

Vite 处理客户端和服务器代码混合使用的方式与 Classic Remix 编译器不同。更多信息,请参阅我们关于 拆分客户端和服务器代码 的文档。

¥Vite handles mixed use of client and server code differently to the Classic Remix compiler. For more information, see our documentation on splitting up client and server code.

新的构建输出路径

¥New build output paths

Vite 管理 public 目录的方式与现有的 Remix 编译器有显著差异。Vite 将文件从 public 目录复制到客户端构建目录,而 Remix 编译器保留 public 目录不变,并使用子目录 (public/build) 作为客户端构建目录。

¥There is a notable difference with the way Vite manages the public directory compared to the existing Remix compiler. Vite copies files from the public directory into the client build directory, whereas the Remix compiler left the public directory untouched and used a subdirectory (public/build) as the client build directory.

为了使默认的 Remix 项目结构与 Vite 的工作方式保持一致,构建输出路径已更改。现在有一个默认为 "build"buildDirectory 选项,取代了单独的 assetsBuildDirectoryserverBuildDirectory 选项。这意味着,默认情况下,服务器现在会编译为 build/server,而客户端现在会编译为 build/client

¥To align the default Remix project structure with the way Vite works, the build output paths have been changed. There is now a single buildDirectory option that defaults to "build", replacing the separate assetsBuildDirectory and serverBuildDirectory options. This means that, by default, the server is now compiled into build/server and the client is now compiled into build/client.

这也意味着以下配置默认值已更改:

¥This also means that the following configuration defaults have been changed:

  • publicPath 已被 Vite "base" 选项 取代,默认为 "/" 而不是 "/build/"

    ¥publicPath has been replaced by Vite's "base" option which defaults to "/" rather than "/build/".

  • serverBuildPath 已被 serverBuildFile 取代,默认为 "index.js"。此文件将被写入你配置的 buildDirectory 中的服务器目录中。

    ¥serverBuildPath has been replaced by serverBuildFile which defaults to "index.js". This file will be written into the server directory within your configured buildDirectory.

Remix 迁移到 Vite 的原因之一是,采用 Remix 后,你需要学习的内容更少。这意味着,对于你想要使用的任何其他打包功能,你应该参考 Vite 文档Vite 插件社区,而不是 Remix 文档。

¥One of the reasons that Remix is moving to Vite is that you have less to learn when adopting Remix. This means that, for any additional bundling features you'd like to use, you should reference Vite documentation and the Vite plugin community rather than the Remix documentation.

Vite 拥有许多现有 Remix 编译器未内置的 featuresplugins 功能。使用任何此类功能都会导致现有的 Remix 编译器无法编译你的应用,因此,如果你打算从现在开始只使用 Vite,则无需使用它们。

¥Vite has many features and plugins that are not built into the existing Remix compiler. The use of any such features will render the existing Remix compiler unable to compile your app, so only use them if you intend to use Vite exclusively from here on out.

迁移

¥Migrating

设置 Vite

¥Setup Vite

👉 安装 Vite 作为开发依赖

¥👉 Install Vite as a development dependency

npm install -D vite

Remix 现在只是一个 Vite 插件,因此你需要将其连接到 Vite。

¥Remix is now just a Vite plugin, so you'll need to hook it up to Vite.

👉 将 Remix 应用根目录下的 remix.config.js 替换为 vite.config.ts

¥👉 Replace remix.config.js with vite.config.ts at the root of your Remix app

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

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

支持的 Remix 配置选项 的子集应直接传递给插件:

¥The subset of supported Remix config options should be passed directly to the plugin:

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

HMR & HDR

Vite 为 HMR 等开发功能提供了强大的客户端运行时,这使得 <LiveReload /> 组件不再适用。在开发环境中使用 Remix Vite 插件时,<Scripts /> 组件将自动包含 Vite 的客户端运行时和其他仅供开发使用的脚本。

¥Vite provides a robust client-side runtime for development features like HMR, making the <LiveReload /> component obsolete. When using the Remix Vite plugin in development, the <Scripts /> component will automatically include Vite's client-side runtime and other dev-only scripts.

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

¥👉 Remove <LiveReload/>, keep <Scripts />

  import {
-   LiveReload,
    Outlet,
    Scripts,
  }

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

TypeScript 集成

¥TypeScript integration

Vite 处理各种不同文件类型的导入,有时处理方式与现有的 Remix 编译器不同,因此我们引用 vite/client 中的 Vite 类型,而不是 @remix-run/dev 中已过时的类型。

¥Vite handles imports for all sorts of different file types, sometimes in ways that differ from the existing Remix compiler, so let's reference Vite's types from vite/client instead of the obsolete types from @remix-run/dev.

由于 vite/client 提供的模块类型与 @remix-run/dev 隐式包含的模块类型不兼容,因此你还需要在 TypeScript 配置中启用 skipLibCheck 标志。一旦 Vite 插件成为默认编译器,Remix 将来将不再需要此标志。

¥Since the module types provided by vite/client are not compatible with the module types implicitly included with @remix-run/dev, you'll also need to enable the skipLibCheck flag in your TypeScript config. Remix won't require this flag in the future once the Vite plugin is the default compiler.

👉 更新 tsconfig.json

¥👉 Update tsconfig.json

更新 tsconfig.json 中的 types 字段,并确保 skipLibCheckmodulemoduleResolution 均已正确设置。

¥Update the types field in tsconfig.json and make sure skipLibCheck, module, and moduleResolution are all set correctly.

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

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

¥👉 Update/remove remix.env.d.ts

移除 remix.env.d.ts 中的以下类型声明

¥Remove the following type declarations in remix.env.d.ts

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

如果 remix.env.d.ts 为空,则删除它

¥If remix.env.d.ts is now empty, delete it

rm remix.env.d.ts

从 Remix 应用服务器迁移

¥Migrating from Remix App Server

如果你在开发中使用 remix-serve(或未使用 -c 标志的 remix dev),则需要切换到新的精简开发服务器。它内置于 Remix Vite 插件中,并在你运行 remix vite:dev 时接管。

¥If you were using remix-serve in development (or remix dev without the -c flag), you'll need to switch to the new minimal dev server. It comes built-in with the Remix Vite plugin and will take over when you run remix vite:dev.

Remix Vite 插件不会安装任何 全局 Node polyfill,因此如果你依赖 remix-serve 提供它们,则需要自行安装。最简单的方法是在 Vite 配置顶部调用 installGlobals

¥The Remix Vite plugin doesn't install any global Node polyfills so you'll need to install them yourself if you were relying on remix-serve to provide them. The easiest way to do this is by calling installGlobals at the top of your Vite config.

Vite 开发服务器的默认端口与 remix-serve 不同,因此如果你想保留相同的端口,则需要通过 Vite 的 server.port 选项进行配置。

¥The Vite dev server's default port is different to remix-serve so you'll need to configure this via Vite's server.port option if you'd like to maintain the same port.

你还需要更新到新的构建输出路径,即服务器的 build/server 和客户端资源的 build/client

¥You'll also need to update to the new build output paths, which are build/server for the server and build/client for client assets.

👉 更新你的 devbuildstart 脚本

¥👉 Update your dev, build and start scripts

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

👉 在 Vite 配置中安装全局 Node polyfill

¥👉 Install global Node polyfills in your Vite config

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 开发服务器端口(可选)

¥👉 Configure your Vite dev server port (optional)

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

迁移自定义服务器

¥Migrating a custom server

如果你在开发中使用自定义服务器,则需要编辑自定义服务器以使用 Vite 的 connect 中间件。这将在开发过程中将资源请求和初始渲染请求委托给 Vite,让你即使使用自定义服务器也能享受 Vite 出色的 DX 功能。

¥If you were using a custom server in development, you'll need to edit your custom server to use Vite's connect middleware. This will delegate asset requests and initial render requests to Vite during development, letting you benefit from Vite's excellent DX even with a custom server.

然后,你可以在开发过程中加载名为 "virtual:remix/server-build" 的虚拟模块,以创建基于 Vite 的请求处理程序。

¥You can then load the virtual module named "virtual:remix/server-build" during development to create a Vite-based request handler.

你还需要更新服务器代码以引用新的构建输出路径,即服务器构建的 build/server 和客户端资源的 build/client

¥You'll also need to update your server code to reference the new build output paths, which are build/server for the server build and build/client for client assets.

例如,如果你使用的是 Express,你可以按照以下步骤操作。

¥For example, if you were using Express, here's how you could do it.

👉 更新你的 server.mjs 文件

¥👉 Update your server.mjs file

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("http://localhost:" + port)
);

👉 更新你的 builddevstart 脚本

¥👉 Update your build, dev, and start scripts

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

如果你愿意,你可以使用 TypeScript 编写自定义服务器。然后,你可以使用 tsxtsm 等工具来运行你的自定义服务器:

¥If you prefer, you can instead author your custom server in TypeScript. You could then use tools like tsx or tsm to run your custom server:

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

请记住,如果这样做,服务器初始启动时速度可能会明显变慢。

¥Remember that there might be some noticeable slowdown for initial server startup if you do this.

迁移 Cloudflare 函数

¥Migrating Cloudflare Functions

Remix Vite 插件仅官方支持 Cloudflare 页面,它专为全栈应用设计,与 Cloudflare Workers Sites 不同。如果你当前使用的是 Cloudflare Workers Sites,请参阅 Cloudflare Pages 迁移指南

👉 在 remix 插件之前添加 cloudflareDevProxyVitePlugin 即可正确覆盖 vite 开发服务器的中间件!

¥👉 add cloudflareDevProxyVitePlugin before remix plugin to correctly override vite dev server's middleware!

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

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

你的 Cloudflare 应用可能正在设置 Remix 配置 server 字段 以生成一个 catch-all Cloudflare 函数。有了 Vite,这种间接访问就不再必要了。你可以直接为 Cloudflare 编写一个 catch-all 路由,就像你为 Express 或任何其他自定义服务器编写一样。

¥Your Cloudflare app may be setting the Remix Config server field to generate a catch-all Cloudflare Function. With Vite, this indirection is no longer necessary. Instead, you can author a catch-all route directly for Cloudflare, just like how you would for Express or any other custom servers.

👉 为 Remix 创建一个 catch-all 路由

¥👉 Create a catch-all route for 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 访问绑定和环境变量

¥👉 Access Bindings and Environment Variables through context.cloudflare.env instead of context.env

虽然你在开发过程中主要使用 Vite,但你也可以使用 Wrangler 预览和部署你的应用。

¥While you'll mostly use Vite during development, you can also use Wrangler to preview and deploy your app.

要了解更多信息,请参阅本文档的 Cloudflare 部分。

¥To learn more, see the Cloudflare section of this document.

👉 更新你的 package.json 脚本

¥👉 Update your package.json scripts

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

迁移引用以构建输出路径

¥Migrate references to build output paths

使用现有 Remix 编译器的默认选项时,服务器会被编译为 build,客户端会被编译为 public/build。由于 Vite 与现有 Remix 编译器处理 public 目录的方式不同,这些输出路径已更改。

¥When using the existing Remix compiler's default options, the server was compiled into build and the client was compiled into public/build. Due to differences with the way Vite typically works with its public directory compared to the existing Remix compiler, these output paths have changed.

👉 更新引用,以构建输出路径

¥👉 Update references to build output paths

  • 服务器现在默认编译为 build/server

    ¥The server is now compiled into build/server by default.

  • 客户端现在默认编译为 build/client

    ¥The client is now compiled into build/client by default.

例如,要从 Blues 技术栈 更新 Dockerfile:

¥For example, to update the Dockerfile from the Blues Stack:

-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

配置路径别名

¥Configure path aliases

Remix 编译器利用 tsconfig.json 中的 paths 选项来解析路径别名。Remix 社区通常使用这种方式将 ~ 定义为 app 目录的别名。

¥The Remix compiler leverages the paths option in your tsconfig.json to resolve path aliases. This is commonly used in the Remix community to define ~ as an alias for the app directory.

Vite 默认不提供任何路径别名。如果你依赖此功能,则可以安装 vite-tsconfig-paths 插件以自动解析 Vite 中 tsconfig.json 的路径别名,与 Remix 编译器的行为一致:

¥Vite does not provide any path aliases by default. If you were relying on this feature, you can install the vite-tsconfig-paths plugin to automatically resolve path aliases from your tsconfig.json in Vite, matching the behavior of the Remix compiler:

👉 安装 vite-tsconfig-paths

¥👉 Install vite-tsconfig-paths

npm install -D vite-tsconfig-paths

👉 将 vite-tsconfig-paths 添加到你的 Vite 配置

¥👉 Add vite-tsconfig-paths to your Vite config

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

¥Remove @remix-run/css-bundle

Vite 内置支持 CSS 副作用导入、PostCSS 和 CSS 模块以及其他 CSS 打包功能。Remix Vite 插件会自动将打包的 CSS 附加到相关路由。

¥Vite has built-in support for CSS side effect imports, PostCSS, and CSS Modules, among other CSS bundling features. The Remix Vite plugin automatically attaches bundled CSS to the relevant routes.

使用 Vite 时,@remix-run/css-bundle 包是多余的,因为它的 cssBundleHref 导出始终是 undefined

¥The @remix-run/css-bundle package is redundant when using Vite since its cssBundleHref export will always be undefined.

👉 卸载 @remix-run/css-bundle

¥👉 Uninstall @remix-run/css-bundle

npm uninstall @remix-run/css-bundle

👉 删除对 cssBundleHref 的引用

¥👉 Remove references to 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,则可以将其完全移除。

¥If a route's links function is only used to wire up cssBundleHref, you can remove it entirely.

- 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 }]
-     : []),
- ];

¥Fix up CSS imports referenced in links

其他形式的 CSS 打包 不需要此功能,例如 CSS 模块、CSS 副作用导入、Vanilla Extract 等。

如果你使用的是 引用 CSS 在 links 函数中,则需要更新相应的 CSS 导入以使用 Vite 显式 ?url 导入语法。

¥If you are referencing CSS in a links function, you'll need to update the corresponding CSS imports to use Vite's explicit ?url import syntax.

👉 将 ?url 添加到 links 中使用的 CSS 导入中。

¥👉 Add ?url to CSS imports used in links

.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

¥Enable Tailwind via PostCSS

如果你的项目正在使用 Tailwind CSS,你首先需要确保你有一个 PostCSS 配置文件,该文件将被 Vite 自动获取。这是因为当 Remix 的 tailwind 选项启用时,Remix 编译器不需要 PostCSS 配置文件。

¥If your project is using Tailwind CSS, you'll first need to ensure that you have a PostCSS config file which will get automatically picked up by Vite. This is because the Remix compiler didn't require a PostCSS config file when Remix's tailwind option was enabled.

👉 如果缺少 PostCSS 配置,请添加,包括 tailwindcss 插件。

¥👉 Add PostCSS config if it's missing, including the tailwindcss plugin

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

如果你的项目已经有 PostCSS 配置文件,则需要添加 tailwindcss 插件(如果尚未安装)。这是因为当 Remix 的 tailwind 配置选项 启用时,Remix 编译器会自动包含此插件。

¥If your project already has a PostCSS config file, you'll need to add the tailwindcss plugin if it's not already present. This is because the Remix compiler included this plugin automatically when Remix's tailwind config option was enabled.

👉 如果缺少 tailwindcss 插件,请将其添加到你的 PostCSS 配置中。

¥👉 Add the tailwindcss plugin to your PostCSS config if it's missing

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

👉 迁移 Tailwind CSS 导入

¥👉 Migrate Tailwind CSS import

如果你是 links 函数中引用你的 Tailwind CSS 文件,则需要 迁移 Tailwind CSS 导入语句。

¥If you're referencing your Tailwind CSS file in a links function, you'll need to migrate your Tailwind CSS import statement.

添加 Vanilla Extract 插件

¥Add Vanilla Extract plugin

如果你使用的是 Vanilla Extract,则需要设置 Vite 插件。

¥If you're using Vanilla Extract, you'll need to set up the Vite plugin.

👉 安装官方 Vite 的 Vanilla Extract 插件

¥👉 Install the official Vanilla Extract plugin for Vite

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

👉 将 Vanilla Extract 插件添加到你的 Vite 配置

¥👉 Add the Vanilla Extract plugin to your Vite config

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 插件

¥Add MDX plugin

如果你使用的是 MDX,由于 Vite 的插件 API 是 Rollup 插件 API 的扩展,因此你应该使用官方的 MDX Rollup 插件

¥If you're using MDX, since Vite's plugin API is an extension of the Rollup plugin API, you should use the official MDX Rollup plugin:

👉 安装 MDX Rollup 插件

¥👉 Install the MDX Rollup plugin

npm install -D @mdx-js/rollup

Remix 插件需要处理 JavaScript 或 TypeScript 文件,因此任何从其他语言(例如 MDX)进行的转译都必须先完成。在这种情况下,这意味着将 MDX 插件放在 Remix 插件之前。

👉 将 MDX Rollup 插件添加到你的 Vite 配置

¥👉 Add the MDX Rollup plugin to your Vite config

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 前置内容支持

¥Add MDX frontmatter support

Remix 编译器允许你定义 MDX 中的 frontmatter。如果你正在使用此功能,则可以在 Vite 中使用 remark-mdx-frontmatter 实现此操作。

¥The Remix compiler allowed you to define frontmatter in MDX. If you were using this feature, you can achieve this in Vite using remark-mdx-frontmatter.

👉 安装所需的 备注 前置插件

¥👉 Install the required Remark frontmatter plugins

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

👉 将 Remark 前置插件传递给 MDX Rollup 插件

¥👉 Pass the Remark frontmatter plugins to the MDX Rollup plugin

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 的导出名称,但我们建议你更新应用代码,改用默认导出名称。

¥In the Remix compiler, the frontmatter export was named attributes. This differs from the frontmatter plugin's default export name of frontmatter. Although it's possible to configure the frontmatter export name, we recommend updating your app code to use the default export name instead.

👉 在 MDX 文件中将 MDX attributes 导出重命名为 frontmatter

¥👉 Rename MDX attributes export to frontmatter within MDX files

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

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

👉 将 MDX attributes 导出重命名为 frontmatter,以供消费者使用

¥👉 Rename MDX attributes export to frontmatter for consumers

  import Component, {
-   attributes,
+   frontmatter,
  } from "./posts/first-post.mdx";

定义 MDX 文件的类型

¥Define types for MDX files

👉 将 *.mdx 文件的类型添加到 env.d.ts

¥👉 Add types for *.mdx files to 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 前置内容映射到路由导出

¥Map MDX frontmatter to route exports

Remix 编译器允许你在 frontmatter 中定义 headersmetahandle 路由导出。remark-mdx-frontmatter 插件不支持此 Remix 特有功能。如果你正在使用此功能,你应该手动将 frontmatter 映射到路由导出:

¥The Remix compiler allowed you to define headers, meta and handle route exports in your frontmatter. This Remix-specific feature is not supported by the remark-mdx-frontmatter plugin. If you were using this feature, you should manually map frontmatter to route exports yourself:

👉 将 frontmatter 映射到 MDX 路由的路由导出

¥👉 Map frontmatter to route exports for MDX routes

---
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 路由导出,因此你现在可以自由使用任何你喜欢的前置内容结构。

¥Note that, since you're explicitly mapping MDX route exports, you're now free to use whatever frontmatter structure you like.

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

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

# Hello World

更新 MDX 文件名用法

¥Update MDX filename usage

Remix 编译器还提供了从所有 MDX 文件导出 filename 的功能。这主要是为了能够链接到 MDX 路由集合。如果你正在使用此功能,你可以在 Vite 中通过 全局导入 实现,它提供了一个便捷的数据结构,可以将文件名映射到模块。这使得维护 MDX 文件列表变得更加容易,因为你不再需要手动导入每个文件。

¥The Remix compiler also provided a filename export from all MDX files. This was primarily designed to enable linking to collections of MDX routes. If you were using this feature, you can achieve this in Vite via glob imports which give you a handy data structure that maps file names to modules. This makes it much easier to maintain a list of MDX files since you no longer need to import each one manually.

例如,要导入 posts 目录中的所有 MDX 文件:

¥For example, to import all MDX files in the posts directory:

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

这相当于手写以下内容:

¥This is equivalent to writing this by hand:

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 文件:

¥You can also eagerly import all MDX files if you'd prefer:

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

调试

¥Debugging

你可以使用 NODE_OPTIONS 环境变量 启动调试会话:

¥You can use the NODE_OPTIONS environment variable to start a debugging session:

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

然后,你可以从浏览器中连接调试器。例如,在 Chrome 中,你可以打开 chrome://inspect 或点击开发工具中的 Node.js 图标来连接调试器。

¥Then you can attach a debugger from your browser. For example, in Chrome you can open up chrome://inspect or click the Node.js icon in the dev tools to attach the debugger.

vite-plugin-inspect

vite-plugin-inspect 向你展示每个 Vite 插件如何转换你的代码以及每个插件的运行时间。

¥vite-plugin-inspect shows you each how each Vite plugin transforms your code and how long each plugin takes.

性能

¥Performance

Remix 包含一个用于性能分析的 --profile 标志。

¥Remix includes a --profile flag for performance profiling.

remix vite:build --profile

使用 --profile 运行时,将生成一个 .cpuprofile 文件,该文件可以共享或上传到 speedscope.app 进行分析。

¥When running with --profile, a .cpuprofile file will be generated that can be shared or upload to speedscope.app to for analysis.

你还可以在开发服务器运行时按下 p + enter 键,在开发环境中进行性能分析,以启动新的性能分析会话或停止当前会话。如果你需要分析开发服务器的启动情况,也可以使用 --profile 标志在启动时初始化分析会话:

¥You can also profile in dev by pressing p + enter while the dev server is running to start a new profiling session or stop the current session. If you need to profile dev server startup, you can also use the --profile flag to initialize a profiling session on startup:

remix vite:dev --profile

请记住,你可以随时查看 Vite 性能文档 以获取更多提示!

¥Remember that you can always check the Vite performance docs for more tips!

Bundle 分析

¥Bundle analysis

要可视化和分析你的包,你可以使用 rollup-plugin-visualizer 插件:

¥To visualize and analyze your bundle, you can use the rollup-plugin-visualizer plugin:

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 时,它会在你的每个 bundles 中生成一个 stats.html 文件:

¥Then when you run remix vite:build, it'll generate a stats.html file in each of your bundles:

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

在浏览器中打开 stats.html 以分析你的软件包。

¥Open up stats.html in your browser to analyze your bundle.

故障排除

¥Troubleshooting

查看 debuggingperformance 部分,了解常规故障排除技巧。此外,请通过查看 GitHub 上 remix vite 插件的已知问题 看看是否有其他人遇到类似的问题。

¥Check the debugging and performance sections for general troubleshooting tips. Also, see if anyone else is having a similar problem by looking through the known issues with the remix vite plugin on GitHub.

HMR

如果你期望热更新但页面却完全重新加载,请查看我们的 关于热模块替换的讨论,了解更多关于 React 快速刷新的局限性以及常见问题的解决方法。

¥If you are expecting hot updates but getting full page reloads, check out our discussion on Hot Module Replacement to learn more about the limitations of React Fast Refresh and workarounds for common issues.

ESM / CJS

Vite 同时支持 ESM 和 CJS 依赖,但有时你仍然可能会遇到 ESM / CJS 互操作问题。通常,这是因为依赖未正确配置以支持 ESM。我们不怪他们,这是 同时正确支持 ESM 和 CJS 非常棘手

¥Vite supports both ESM and CJS dependencies, but sometimes you might still run into issues with ESM / CJS interop. Usually, this is because a dependency is not properly configured to support ESM. And we don't blame them, it's really tricky to support both ESM and CJS properly.

有关修复示例错误的演练,请查看 🎥 如何修复 Remix 中的 CJS/ESM 错误

¥For a walkthrough of fixing an example bug, check out 🎥 How to Fix CJS/ESM Bugs in Remix.

要诊断你的某个依赖是否配置错误,请检查 publint类型是否错误。此外,你可以使用 vite-plugin-cjs-interop 插件 平滑解决 default 导出外部 CJS 依赖的问题。

¥To diagnose if one of your dependencies is misconfigured, check publint or Are The Types Wrong. Additionally, you can use the vite-plugin-cjs-interop plugin smooth over issues with default exports for external CJS dependencies.

最后,你还可以明确配置要将哪些依赖与 Vite ssr.noExternal 选项 打包到服务器中,以便使用 Remix Vite 插件模拟 Remix 编译器的 serverDependenciesToBundle

¥Finally, you can also explicitly configure which dependencies to bundle into your server bundled with Vite's ssr.noExternal option to emulate the Remix compiler's serverDependenciesToBundle with the Remix Vite plugin.

开发过程中浏览器中的服务器代码错误

¥Server code errors in browser during development

如果你在开发过程中在浏览器控制台中看到指向服务器代码的错误,则可能需要安装 明确隔离服务器端代码。例如,如果你看到类似以下内容:

¥If you see errors in the browser console during development that point to server code, you likely need to explicitly isolate server-only code. For example, if you see something like:

Uncaught ReferenceError: process is not defined

然后,你需要跟踪哪个模块正在引入除服务器端全局变量(例如 process)之外的依赖,并在 单独的 .server 模块或与 vite-env-only 一起使用 中隔离代码。由于 Vite 在生产环境中使用 Rollup 对你的代码进行树状图优化,因此这些错误只会在开发环境中发生。

¥Then you'll need to track down which module is pulling in dependencies that except server-only globals like process and isolate code either in a separate .server module or with vite-env-only. Since Vite uses Rollup to treeshake your code in production, these errors only occur in development.

与其他基于 Vite 的工具(例如 Vitest、Storybook)的插件使用

¥Plugin usage with other Vite-based tools (e.g., Vitest, Storybook)

Remix Vite 插件仅适用于应用的开发服务器和生产版本。虽然还有其他基于 Vite 的工具(例如 Vitest 和 Storybook)使用 Vite 配置文件,但 Remix Vite 插件并非为与这些工具配合使用而设计的。我们目前建议在与其他基于 Vite 的工具一起使用时排除该插件。

¥The Remix Vite plugin is only intended for use in your application's development server and production builds. While there are other Vite-based tools such as Vitest and Storybook that make use of the Vite config file, the Remix Vite plugin has not been designed for use with these tools. We currently recommend excluding the plugin when used with other Vite-based tools.

对于 Vitest:

¥For 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:

¥For 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 配置:

¥Alternatively, you can use separate Vite config files for each tool. For example, to use a Vite config specifically scoped to Remix:

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

当未提供 Remix Vite 插件时,你的设置可能还需要提供 Vite React 插件。例如,使用 Vitest 时:

¥When not providing the Remix Vite plugin, your setup might also need to provide Vite Plugin React. For example, when using 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(), ""),
  },
});

document 重新挂载时,样式会在开发过程中消失

¥Styles disappear in development when document remounts

当使用 React 渲染整个文档(例如 Remix 所做的那样)时,将元素动态注入 head 元素时可能会遇到问题。如果文档被重新挂载,现有的 head 元素将被删除并替换为一个全新的元素,从而删除 Vite 在开发过程中注入的所有 style 元素。

¥When React is used to render the entire document (as Remix does) you can run into issues when elements are dynamically injected into the head element. If the document is re-mounted, the existing head element is removed and replaced with an entirely new one, removing any style elements that Vite injects during development.

这是一个已知的 React 问题,已在其 预览版发布渠道 中修复。如果你了解所涉及的风险,你可以将你的应用固定到特定的 React 版本,然后使用 软件包覆盖 来确保这是整个项目中使用的唯一 React 版本。例如:

¥This is a known React issue fixed in their canary release channel. If you understand the risks involved, you can pin your app to a specific React version and then use package overrides to ensure this is the only version of React used throughout your project. For example:

{
  "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 文件。

¥It's worth stressing that this issue with styles that were injected by Vite only happens in development. Production builds won't have this issue since static CSS files are generated.

在 Remix 中,此问题可能在 根路由的默认组件导出 及其 ErrorBoundary 和/或 HydrateFallback 导出之间交替渲染时出现,因为这会导致挂载新的文档级组件。

¥In Remix, this issue can surface when rendering alternates between your root route's default component export and its ErrorBoundary and/or HydrateFallback exports since this results in a new document-level component being mounted.

由于 Hydration 错误,React 可能会从头开始重新渲染整个页面,因此也可能发生这种情况。Hydration 错误可能是由你的应用代码引起的,也可能是由操作文档的浏览器扩展程序引起的。

¥It can also happen due to hydration errors since it causes React to re-render the entire page from scratch. Hydration errors can be caused by your app code, but they can also be caused by browser extensions that manipulate the document.

这与 Vite 相关,因为在开发过程中,Vite 会将 CSS 导入转换为 JS 文件,并将其样式注入文档中作为副作用。Vite 这样做是为了支持静态 CSS 文件的延迟加载和 HMR。

¥This is relevant for Vite because — during development — Vite transforms CSS imports into JS files that inject their styles into the document as a side effect. Vite does this to support lazy-loading and HMR of static CSS files.

例如,假设你的应用具有以下 CSS 文件:

¥For example, let's assume your app has the following CSS file:


* { margin: 0 }

在开发过程中,此 CSS 文件在导入时将作为副作用转换为以下 JavaScript 代码:

¥During development, this CSS file will be transformed into the following JavaScript code when imported as a side effect:

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

此转换不适用于生产代码,因此此样式问题仅影响开发阶段。

¥This transformation is not applied to production code, which is why this styling issue only affects development.

开发中的 Wrangler 错误

¥Wrangler errors in development

使用 Cloudflare Pages 时,你可能会遇到来自 wrangler pages dev 的以下错误:

¥When using Cloudflare Pages, you may encounter the following error from 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 的已知问题

¥This is a known issue with Wrangler.

致谢

¥Acknowledgements

Vite 是一个非常棒的项目,我们感谢 Vite 团队的辛勤工作。特别感谢 来自 Vite 的 Matias Capeletto、Arnaud Barré 和 Bjorn Lu 团队 的指导。

¥Vite is an amazing project, and we're grateful to the Vite team for their work. Special thanks to Matias Capeletto, Arnaud Barré, and Bjorn Lu from the Vite team for their guidance.

Remix 社区快速探索了 Vite 支持,我们感谢他们的贡献:

¥The Remix community was quick to explore Vite support, and we're grateful for their contributions:

最后,我们借鉴了其他框架实现 Vite 支持的方式:

¥Finally, we were inspired by how other frameworks implemented Vite support:

Remix v2.17 中文网 - 粤ICP备13048890号