Remix 决定用 vite 代替 esbuild
Vite 支持目前还不稳定,仅用于收集早期反馈。我们还不建议在生产环境中使用它。
Vite[1]是一个强大、高性能且可扩展的 JavaScript 项目开发环境。为了改进和扩展 Remix 的打包功能,我们目前正在探索使用 Vite 作为 esbuild 的替代编译器。
图例:✅(已测试),❓(未测试),⏳(尚未支持)
| 功能 | Node | Deno | Cloudflare | 备注 |
|---|---|---|---|---|
| 内置 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.tsxexport 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/denoimport 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/denoexport 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
参考资料
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
@remix-run/css-bundle: ../styling/bundling
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
React 需要 key: https://react.dev/learn/rendering-lists#why-does-react-need-keys
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/