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 提供了 指向本地 workerd 绑定的 Node 代理。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 传递给 functions/[[path]].ts 中的 Cloudflare 代理插件和请求处理程序**两者**,否则,根据你运行应用程序的方式,你将获得不一致的加载上下文扩展。

首先,将 getLoadContext 传递给 Vite 配置中的 Cloudflare 代理插件,以便在运行 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 时扩展加载上下文

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 中。

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

Remix 迁移到 Vite 的原因之一是为了简化 Remix 的学习曲线。这意味着,如果您想使用任何额外的打包功能,应该参考 Vite 文档Vite 插件社区,而不是 Remix 文档。

Vite 有许多 功能插件,这些功能和插件没有内置到现有的 Remix 编译器中。使用任何此类功能将导致现有的 Remix 编译器无法编译您的应用程序,因此,只有在您打算从现在开始完全使用 Vite 时才使用它们。

迁移

设置 Vite

👉 安装 Vite 作为开发依赖项

npm install -D vite

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

👉 remix.config.js 替换为 Remix 应用根目录下的 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 应用服务器迁移

如果您在开发中使用 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 优秀的开发体验中获益。

然后,您可以在开发期间加载名为 "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 函数。使用 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 与现有 Remix 编译器相比,在处理 public 目录的方式上存在差异,因此这些输出路径已更改。

👉 更新对构建输出路径的引用

  • 服务器现在默认编译到 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 Modules 等 CSS 打包功能。Remix Vite 插件会自动将打包后的 CSS 附加到相关的路由上。

当使用 Vite 时,@remix-run/css-bundle包是冗余的,因为它的 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 副作用导入、Vanilla Extract 等),不需要此操作。

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

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

.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 编译器在 Remix 的 tailwind 配置选项 启用时会自动包含此插件。

👉 如果缺少 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 前置 matter 支持

Remix 编译器允许您在 MDX 中定义 前置 matter。如果您正在使用此功能,则可以使用 remark-mdx-frontmatter 在 Vite 中实现此功能。

👉 安装所需的 Remark 前置 matter 插件

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

👉 将 Remark 前置 matter 插件传递给 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 映射到路由导出。

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

---
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 路由的集合。如果您正在使用此功能,则可以通过 全局导入 在 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 来摇树您的代码,因此这些错误仅在开发环境中出现。

插件与其他基于 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 问题,已在其 金丝雀发布渠道 中修复。如果您了解所涉及的风险,您可以将您的应用程序固定到特定的 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