搜狐技术产品

Remix 决定用 vite 代替 esbuild

Vite 支持目前还不稳定,仅用于收集早期反馈。我们还不建议在生产环境中使用它。

Vite[1]是一个强大、高性能且可扩展的 JavaScript 项目开发环境。为了改进和扩展 Remix 的打包功能,我们目前正在探索使用 Vite 作为 esbuild 的替代编译器。

图例:✅(已测试),❓(未测试),⏳(尚未支持)

功能NodeDenoCloudflare备注
内置 dev 服务器✅❓⏳
其他服务器(如 Express)⏳⏳⏳
HMR✅❓⏳
HDR✅❓⏳
MDX 路由✅❓⏳存在一些不推荐使用的情况。[2]

开始使用

要在现有 Remix 项目中开始使用 Vite(或使用 create-remix[3] 创建的新项目),首先将 Vite 作为开发依赖项安装:

npm install -D vite

然后在项目根目录下添加 vite.config.ts,并在 plugins 数组中提供 Remix 插件:

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

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

Vite 插件接受以下 Remix 配置子集:

注意,除非你在 Vite 配置中手动导入并传递给插件,否则 remix.config.js 不会被 Remix Vite 插件使用。

  • appDirectory[4]
  • assetsBuildDirectory[5]
  • ignoredRouteFiles[6]
  • publicPath[7]
  • routes[8]
  • serverBuildPath[9]
  • serverModuleFormat[10]

例如:

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

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

所有其他与打包相关的选项现在都可以通过 Vite 进行配置[11]。这意味着你可以对打包过程进行更详细的控制。

要启动开发服务器,直接运行 Vite 的 dev 命令。

vite dev

要运行生产构建,首先为客户端运行 Vite 的 build 命令,然后为服务端使用 --ssr 标志运行。

vite build && vite build --ssr

使用 Vite 的区别

由于 Vite 现在负责打包你的应用程序,与 Remix 编译器相比,存在一些区别需要注意。

<LiveReload /> 在 <Scripts /> 之前

在最初不稳定的版本中,Remix Vite 插件假定 <LiveReload /> 组件位于 <Scripts /> 之前,以便 <Live Reload /> 中的 React Fast Refresh 初始化最先发生。

如果 <Scripts /> 位于 <LiveReload /> 之前,则 React Fast Refresh 将无法执行 HMR[12]。

// app/root.tsx

export default function App() {
  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <Meta />
        <Links />
      </head>
      <body>
        <Outlet />
        <ScrollRestoration />
+       <LiveReload />
        <Scripts />
-       <LiveReload />
      </body>
    </html>
  );
}

在作为稳定版本发布之前,我们会重新设计这些 API,解决顺序问题。

新的打包功能

Vite 具有许多 Remix 编译器中不存在的功能[13]和插件[14]。使用这些功能的任何用法都会打破与 Remix 编译器的向后兼容性,且只能在仅使用 Vite 时使用。

TypeScript

在 .d.ts 文件中添加 vite/client 类型。我们建议用新的 env.d.ts 文件替换现有的 remix.env.d.ts 文件:

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

路径别名

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

Vite 默认没有提供任何路径别名。你可以安装 vite-tsconfig-paths[15] 插件,在 Vite 中自动解析 tsconfig.json 中的路径别名,与 Remix 编译器的行为匹配:

npm install -D vite-tsconfig-paths

然后将其添加到 Vite 配置中:

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

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

或者,你可以不引用 tsconfig.json,直接使用 Vite 的 `resolve.alias`[16] 选项定义路径别名:

import { fileURLToPath, URL } from "node:url";

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

export default defineConfig({
  resolve: {
    alias: {
      "~": fileURLToPath(new URL("./app", import.meta.url)),
    },
  },
  plugins: [remix()],
});

常规 CSS 导入

在 Vite 中导入 CSS 文件时,默认导出为字符串形式的文件内容。这与 Remix 编译器提供文件的 URL 不同。在 Vite 中导入 CSS 文件的 URL,你需要在导入路径的末尾显式添加 ?url:

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

例如:

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

import styles from "./dashboard.css?url";

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

如果在同一项目中使用 Vite 和 Remix 编译器,可以在 Remix Vite 插件中启用 legacyCssImports,它会自动在所有相关 CSS 导入中附加 ?url:

此选项仅用于过渡到 Vite,将来会删除。

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

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

CSS 打包

Vite 内置了对 CSS 副作用导入、PostCSS 和 CSS Modules 等 CSS 打包功能的支持。Remix Vite 插件会自动将打包后的 CSS 附加到相关路由,所以不再需要 `@remix-run/css-bundle`[17] 包。

如果在同一项目中使用 Vite 和 Remix 编译器,你可以继续使用 @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 }] : []),
  // ...
];

Tailwind

要在 Vite 中使用 Tailwind[18],首先安装所需的依赖项:

npm install -D tailwindcss postcss autoprefixer

然后为 Tailwind 和 PostCSS 生成配置文件:

npx tailwindcss init --ts -p

如果你的 Remix 项目已经有 PostCSS 配置文件,你需要确保已经配置了 tailwindcss 插件。此插件之前由 Remix 编译器注入(如果缺失)。

现在我们可以告诉它从哪些文件生成样式:

import type { Config } from "tailwindcss";

export default {
  content: ["./app/**/*.{js,jsx,ts,tsx}"],
  theme: {
    extend: {},
  },
  plugins: [],
} satisfies Config;

然后在 app CSS 的某个地方包含 @tailwind 指令。例如,你可以在 app 根目录创建一个 tailwind.css 文件:

@tailwind base;
@tailwind components;
@tailwind utilities;

Vanilla Extract

要在 Vite 中使用 Vanilla Extract[19],安装官方的 Vite 插件[20]。

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

然后将插件添加到 Vite 配置中:

import { unstable_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

由于 Vite 的插件 API 是 Rollup 插件 API 的扩展,你可以使用官方的 MDX Rollup 插件[21]:

npm install -D @mdx-js/rollup

然后将 Rollup 插件添加到 Vite 配置中:

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

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

MDX Frontmatter

Remix 编译器允许你在 MDX 中定义 frontmatter[22]。在 Vite 中,你可以使用 remark-mdx-frontmatter[23] 来实现这一点。

首先,安装所需的 Remark[24] 插件:

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

然后将这些插件提供给 MDX Rollup 插件:

import mdx from "@mdx-js/rollup";
import { unstable_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: [
    remix(),
    mdx({
      remarkPlugins: [remarkFrontmatter, remarkMdxFrontmatter],
    }),
  ],
});

在 Remix 编译器中,frontmatter 导出被命名为 attributes。这与 frontmatter 插件的默认导出名 frontmatter 不同。为了与 Remix 编译器保持向后兼容性,你可以通过 name 选项覆盖此名称:

import mdx from "@mdx-js/rollup";
import { unstable_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: [
    remix(),
    mdx({
      remarkPlugins: [
        remarkFrontmatter,
        [remarkMdxFrontmatter, { name: "attributes" }],
      ],
    }),
  ],
});

MDX 路由 Frontmatter

Remix 编译器允许你在 frontmatter 中定义 headers、meta 和 handle 路由导出。这一 Remix 特有的功能显然不被 remark-mdx-frontmatter 插件支持,但你可以自己手动将 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 路由集合。在 Vite 中,你应该通过 glob 导入[25]来实现这一点,它为你提供了一个方便的数据结构,将文件名映射到模块。这使维护 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,
});

HMR & HDR

React Fast Refresh 限制

React Fast Refresh[26] 有一些限制需要注意。

类组件状态

React Fast Refresh 不会保留类组件的状态。这包括内部返回类的高阶组件:

export class ComponentA extends Component {} // ❌

export const ComponentB = HOC(ComponentC); // ❌ 如果 HOC 返回一个类组件就不会起作用

export function ComponentD() {} // ✅
export const ComponentE = () => {}; // ✅
export default function ComponentF() {} // ✅

命名的函数组件

函数组件必须命名,不能是匿名的,以便 React Fast Refresh 跟踪更改:

export default () => {}; // ❌
export default function () {} // ❌

const ComponentA = () => {};
export default ComponentA; // ✅

export default function ComponentB() {} // ✅

支持的导出

React Fast Refresh 只能处理组件导出。虽然 Remix 会为你管理诸如 meta、links 和 header 等特殊路由导出,但任何用户定义的导出都会导致完全重新加载:

// 这些导出由 Remix Vite 插件处理
// 以实现 HMR 兼容性
export const meta = { title: "Home" }; // ✅
export const links = [{ rel: "stylesheet", href: "style.css" }]; // ✅

// 这些导出被 Remix Vite 插件移除
// 所以它们永远不会影响 HMR
export const headers = { "Cache-Control": "max-age=3600" }; // ✅
export const loader = () => {}; // ✅
export const action = () => {}; // ✅

// 这既不是 Remix 导出,也不是组件导出
// 所以它会导致该路由完全重新加载
export const myValue = "some value"; // ❌

export default function Route() {} // ✅

路由不应该导出随机值。如果你想在路由之间重用值,可以将它们放在自己的非路由模块中:

// my-custom-value.ts
export const myValue = "some value";
添加和删除钩子

当钩子被添加或删除时,React Fast Refresh 无法跟踪组件的更改,导致仅为下一次渲染进行完全重载。更新钩子后,更改应该再次导致热更新。例如,如果你向组件添加 `useLoaderData`[27],你可能会丢失该组件本身的状态。

组件键

在某些情况下,React 无法区分现有组件的更改和新增组件。React 需要 `key`[28] 来区分这些情况并在同级元素被修改时跟踪更改。

致谢

Vite 是一个迷人的项目,我们非常感谢 Vite 团队的工作。特别感谢 Vite 团队的 Matias Capeletto、Arnaud Barré 和 Bjorn Lu[29] 的指导。

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

  • 讨论:考虑使用 Vite[30]
  • remix-kit[31]
  • remix-vite[32]
  • vite-plugin-remix[33]

最后,我们受到了其他框架如何实现 Vite 支持的启发:

  • Astro[34]
  • SolidStart[35]
  • SvelteKit[36]

Vite 派对我们迟到了了,但是现在也不晚!

参考:https://remix.run/docs/en/dev/future/vite

参考资料

[1]

Vite: https://vitejs.dev

[2]

存在一些不推荐使用的情况。: #mdx

[3]

create-remix: ../other-api/create-remix

[4]

appDirectory: ../file-conventions/remix-config#appdirectory

[5]

assetsBuildDirectory: ../file-conventions/remix-config#assetsbuilddirectory

[6]

ignoredRouteFiles: ../file-conventions/remix-config#ignoredroutefiles

[7]

publicPath: ../file-conventions/remix-config#publicpath

[8]

routes: ../file-conventions/remix-config#routes

[9]

serverBuildPath: ../file-conventions/remix-config#serverbuildpath

[10]

serverModuleFormat: ../file-conventions/remix-config#servermoduleformat

[11]

Vite 进行配置: https://vitejs.dev/config

[12]

React Fast Refresh 将无法执行 HMR: https://github.com/facebook/react/issues/16604#issuecomment-528663101

[13]

功能: https://vitejs.dev/guide/features.html

[14]

插件: https://vitejs.dev/plugins

[15]

vite-tsconfig-paths: https://github.com/aleclarson/vite-tsconfig-paths

[16]

resolve.alias: https://vitejs.dev/config/shared-options.html#resolve-alias

[17]

@remix-run/css-bundle: ../styling/bundling

[18]

Tailwind: https://tailwindcss.com

[19]

Vanilla Extract: https://vanilla-extract.style

[20]

Vite 插件: https://vanilla-extract.style/documentation/integrations/vite

[21]

MDX Rollup 插件: https://mdxjs.com/packages/rollup

[22]

frontmatter: https://mdxjs.com/guides/frontmatter

[23]

remark-mdx-frontmatter: https://github.com/remcohaszing/remark-mdx-frontmatter

[24]

Remark: https://remark.js.org

[25]

glob 导入: https://vitejs.dev/guide/features.html#glob-import

[26]

React Fast Refresh: https://github.com/facebook/react/tree/main/packages/react-refresh

[27]

useLoaderData: ../hooks/use-loader-data

[28]

React 需要 key: https://react.dev/learn/rendering-lists#why-does-react-need-keys

[29]

Vite 团队的 Matias Capeletto、Arnaud Barré 和 Bjorn Lu: https://vitejs.dev/team.html

[30]

讨论:考虑使用 Vite: https://github.com/remix-run/remix/discussions/2427

[31]

remix-kit: https://github.com/jrestall/remix-kit

[32]

remix-vite: https://github.com/sudomf/remix-vite

[33]

vite-plugin-remix: https://github.com/yracnet/vite-plugin-remix

[34]

Astro: https://astro.build/

[35]

SolidStart: https://start.solidjs.com/getting-started/what-is-solidstart

[36]

SvelteKit: https://kit.svelte.dev/