简体中文
  • parcel
  • vite
  • js
  • esbuild
  • bundler
  • monorepo

从 Parcel 到 Vite: 一个 10 万行代码迁移的小故事

我们将三个前端项目从 Parcel 迁移到了 Vite,过程非常顺利。

Gao
Gao
Founder

背景故事

我们在 Logto 有三个主要的前端项目:登录体验、控制台,以及实时预览。这些项目都是基于 TypeScript,React 和 SASS 模块构建的;总共有大约 10 万行代码。

我们喜欢 Parcel 的简单性和零配置开发。我还记得第一次用 Parcel 创建新项目时的震惊,你只需运行 parcel index.html 然后咔一声,所有的依赖自动安装并且项目启动起来了。如果你是一个"经验丰富的"开发者,你可能会和我一样有同样的感受,尤其是和以前使用 Gulp 和 Webpack 的日子相比。Parcel 就像一根魔杖。

Parcel 的简单性是我们长期坚持使用它的主要原因,尽管它有时候有点"任性"。例如:

  • Parcel 有时无法打包项目,因为它找不到一些实际上存在的块文件。
  • 在我们的 monorepo 设置下,它需要一些“曲折”的配置才能正常工作。
  • 它不原生支持 MDX 3,因此我们不得不为其创建自定义转换器。
  • 它不支持手动分块(截至本文撰写时,手动分块功能仍处于实验阶段),这在大多数情况下没问题,但有时你会需要它。

那么我们为什么决定迁移到其他东西呢?

  1. 我们卡在了 Parcel 2.9.3,这个版本于 2023 年 6 月发布。此后每次有新版本发布时,我们都尝试升级,但总是出现构建错误。
  2. Parcel 的最新版本是 2.12.0,于 2024 年 2 月发布。尽管它几乎每天有提交更新,但从那以后就没有发布新版本。

有人甚至打开了一个讨论,问Parcel 是否已经死了。官方的回答是否定的,Parcel 仍然活着,但它处于一种"我们正在进行大型重构,没有时间发布小版本"的状态。对我们来说,它就像一只"鸭死":我们能用的最新版本已经是一年前的东西了,而且我们不知道下一个版本何时发布。它看起来像是死了,行为像是死了,因此对我们来说它就死了。

Parcel 升级请求

相信我,我们尽力了。

为什么选择 Vite?

我们通过 Vitest 认识了 Vite。几个月前,我们对 Jest 的 ESM 支持(在测试中)感到厌倦,想试试点新东西。Vitest 凭借其原生 ESM 支持和 Jest 兼容性赢得了我们的心。它有着出色的开发者体验,而且由 Vite 驱动。

现状

你可能在项目中有不同的设置,但通常你会发现插件替代品,因为 Vite 生态系统正在蓬勃发展。这是我们迁移时的一些配置:

  • **Monorepo:**我们使用 PNPM (v9) 工作区管理我们的 Monorepo。
  • **模块:**我们所有的项目都使用 ESM 模块。
  • **TypeScript:**我们使用 TypeScript (v5.5.3) 进行所有的项目开发,并使用路径别名。
  • **React:**我们的所有前端项目都使用 React (v18.3.1)。
  • **样式:**我们使用 SASS 模块来进行样式处理。
  • **SVG:**我们将 SVG 用作 React 组件。
  • **MDX:**我们在 MDX 中支持 GitHub 风格的 Markdown 和 Mermaid。
  • **懒加载:**我们需要懒加载某些页面和组件。
  • **压缩:**我们为生产版本生成压缩后的资源(gzip 和 brotli)。

迁移过程

我们开始迁移时,通过创建一个新的 Vite 项目并尝试它的工作原理来熟悉它。过程很顺利,实际的迁移只花了几天时间。

开箱即用的支持

Vite 对 Monorepo,ESM,TypeScript,React 和 SASS 有开箱即用的支持。我们只需要安装必要的插件和配置来使其工作。

路径别名

Vite 对路径别名有内置支持,例如,在我们的 tsconfig.json 中:

我们只需要在 vite.config.ts 中添加相同的解析:

需要注意的是,替换路径应该是绝对路径,而它是相对于项目根目录的。或者,你可以使用 vite-tsconfig-paths 插件从 tsconfig.json 读取路径别名。

React 快速刷新和 HMR

尽管 Vite 内置了 HMR 支持,但仍需安装一个插件来启用 React 快速刷新。我们使用了由 Vite 团队提供的 @vitejs/plugin-react 插件,它对 React 功能有很好的支持,如快速刷新:

作为 React 组件的 SVG

我们使用 vite-plugin-svgr 插件将 SVG 转换为 React 组件。只需在 Vite 配置中添加插件:

然而,我们并没有指定在什么条件下 SVG 应该被转换为 React 组件,因此所有的导入都被转换了。插件提供了一个更好的默认配置:仅转换导入了.svg?react 后缀的 SVG。我们相应地更新了导入。

SASS 模块

虽然 Vite 对 SASS 模块提供了开箱即用的支持,但有一点需要注意:类名的格式。这可能会对用户和我们的集成测试造成困扰,如果类名没有一致地格式化。在 vite.config.ts 中的一行配置可以解决这个问题:

顺便说一句,Parcel 和 Vite 在导入 SASS 文件时有不同的做法:

* as 语法虽然在 Vite 中也能用,但当你使用动态键访问 styles 对象时,它会导致模块化类名的丢失。例如:

MDX 支持

由于 Vite 在内部使用的是 Rollup,因此我们可以使用官方的 @mdx-js/rollup 插件来支持 MDX 及其插件。配置如下:

remarkGfm 插件用于支持 GitHub 风格的 Markdown,rehypeMdxCodeProps 插件用于将属性传递到 MDX 文件中的代码块,就像 Docusaurus 做的那样。

MDX 中的 Mermaid 支持

我们希望在 MDX 文件中使用 Mermaid 图表,像其他编程语言一样。用法应与其他代码块一样简单:

应该渲染为:

由于我们的应用程序支持浅色和深色主题,我们编写了一些代码来让 Mermaid 图表在深色主题下也能正常工作。创建了一个 React 组件:

useTheme 是一个自定义 hook,用于从上下文中获取当前主题。mermaid 库异步导入,以减少初次页面加载时的加载大小。

对于 MDX 文件中的代码块,我们有一个统一的组件来处理这些工作:

最后我们定义了 MDX 提供者如下:

懒加载

这不是一个 Vite 特有的事情,但仍值得一提的是我们在迁移过程中更新了页面以使用懒加载,迁移后什么也没有坏掉。

React 内置了 React.lazy 函数用于懒加载组件。然而,当你进行快速迭代时,它可能会引发一些问题。我们制作了一个小库叫做 react-safe-lazy 来解决这些问题。它是 React.lazy 的即插即用替代品,详细解释可以在这个 博客帖子 中找到。

压缩

有一个很棒的插件叫做 vite-plugin-compression 可以生成压缩资产。它支持 gzip 和 brotli 压缩。配置非常简单:

手动分块

Vite(或其底层的 Rollup)的一大亮点是手动分块。虽然 React.lazy 是懒加载组件的使用方法,但我们可以通过指定手动分块来更好地控制分块,决定哪些组件或模块应该打包在一起。

例如,我们可以先使用 vite-bundle-visualizer 来分析包大小和依赖关系。然后我们可以编写一个合适的函数来分离这些块:

开发服务器

与生产构建不同,Vite 不会在开发模式下打包你的源码(包括在同一个 monorepo 中链接的依赖),而是将每个模块都当作一个文件。对我们来说,浏览器第一次加载时可能会加载数百个模块,这看起来很疯狂,但在大多数情况下实际上没什么问题。你可以在 这里 看到相关讨论。

如果你对此有所顾虑,一个不完美但可行的解决方案是将链接的依赖列在 vite.config.tsoptimizeDeps 选项中:

这会"预打包"链接的依赖,使开发服务器更快。但是这个方法的缺点是 HMR 可能不会对这些链接依赖如预期那样工作。

另外,我们使用了一个代理,它在生产中提供静态文件并在开发环境中代理到 Vitest 服务器。我们配置了一些特定的端口以避免冲突,这在 vite.config.ts 中也很容易设置:

环境变量

与 Parcel 不同,Vite 使用一种现代化的方法处理环境变量,即使用 import.meta.env。它会自动加载 .env 文件并替换代码中的变量。然而,它要求所有环境变量都以 VITE_ 为前缀(可配置)。

当我们使用 Parcel 时,它只是简单地替换 process.env 变量而不检查前缀。因此,我们提出了一个使用 define 字段的变通方案来使迁移更容易:

这让我们可以逐步将环境变量添加前缀,并删除 define 字段。

结论

就是这些!我们已经成功将三个前端项目从 Parcel 迁移到了 Vite,希望这个小故事能帮助你进行迁移。最终配置如下: