繁體中文(台灣)
  • parcel
  • vite
  • js
  • esbuild
  • bundler
  • monorepo

從 Parcel 到 Vite:一個十萬行代碼遷移的短篇故事

我們已經將三個前端專案從 Parcel 遷移到 Vite,過程非常順利。

Gao
Gao
Founder

背景故事

在 Logto,我們有三個主要的前端專案:登入體驗、控制台和即時預覽。這些專案全部使用 TypeScript、React 和 SASS 模組;總共大約有十萬行代碼。

我們喜歡 Parcel 的簡單和零配置。我仍然記得第一次使用 Parcel 時被它的簡易設置驚艷到了。你只需運行 parcel index.html,然後,所有的必要依賴項都已安裝,專案就能運行了。如果你是個「經驗豐富」的開發者,可能會覺得相較於以往使用 Gulp 和 Webpack 的設置,這是天壤之別。Parcel 就像一根魔杖。

Parcel 的簡單是我們長期使用它的主要原因,即使它有時候脾氣不太好。例如:

  • 有時 Parcel 會因為找不到實際上存在的 chunk 文件而無法完成打包。
  • 它需要一些複雜的配置來與我們的 monorepo 設置配合運作。
  • 它本身並不支持 MDX 3,因此我們必須為其建立自訂轉換器。
  • 它不支持手動 chunks(在撰寫本文時,手動 chunks 功能仍在實驗階段),對大多數情況來說還好,但有時你會需要它。

那麼為什麼我們決定遷移到其他工具?

  1. 我們被鎖定在 2023 年 6 月發佈的 Parcel 2.9.3 每次新版本發佈後,我們都嘗試升級,但總是因為構建錯誤而失敗。
  2. 最新的 Parcel 版本是 2.12.0,發佈於 2024 年 2 月。儘管幾乎每天都有更新提交,但自此以來再也沒有新的釋出。

有人甚至發起了一個討論來詢問Parcel 是否已經死了。官方的回答是否定的,Parcel 仍然活著,但處於「我們正在進行一次大型重構,沒有時間推出小更新」的階段。對我們來說,這就像是「鴨子死」:最新的可用版本已經是一年前的,而我們不知道下一個版本會在何時釋出。它看起來像死了,行為像死了,對我們來說它就是死了。

Parcel 升級的拉取請求

相信我,我們努力過了。

為什麼選擇 Vite?

我們從 Vitest 認識了 Vite。幾個月前,我們對 Jest 的 ESM 支持(在測試中)感到厭倦,想要試試新東西。Vitest 憑藉本地的 ESM 支持和 Jest 兼容贏得了我們的心。它提供了出色的開發者體驗,並且是由 Vite 驅動的。

現狀

你可能有不同的專案設置,但通常你可以在 Vite 生態系統中找到插件替換品。以下是我們進行遷移時的設置:

  • Monorepo: 我們使用 PNPM (v9) workspaces 來管理我們的 monorepo。
  • 模組: 我們為所有專案使用 ESM 模組。
  • TypeScript: 我們為所有專案使用 TypeScript (v5.5.3) 及路徑別名。
  • React: 我們為所有前端應用使用 React (v18.3.1)。
  • 樣式: 我們使用 SASS 模組來進行樣式定義。
  • SVG: 我們將 SVG 轉為 React 組件進行使用。
  • MDX: 我們支持帶有 GitHub 風格 Markdown 及 Mermaid 插圖的 MDX。
  • 延後加載: 我們需要延後加載部分網頁和組件。
  • 壓縮: 我們為生產構建生成壓縮資源(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 特性如快速刷新:

SVG 作為 React 組件

我們使用 vite-plugin-svgr 插件來將 SVG 轉換成 React 組件。只需在 Vite 配置中添加該插件即可:

然而,我們並未指定在什麼條件下將 SVG 轉為 React 組件,因此所有導入項都被轉換了。該插件提供了一個更好的預設配置:只轉換以.svg?react後綴導入的 SVG。我們據此更新了導入。

SASS 模組

雖然 Vite 有內建支持 SASS 模組,但我們需要留意 class 名稱的格式。若名稱格式不一致,可能會造成用戶和我們的整合測試的麻煩。一條配置即可在 vite.config.ts 解決問題:

順帶一提,Parcel 和 Vite 在引入 SASS 文件時有不同的風格:

* as 語法在 Vite 中仍然有效,但使用動態鍵訪問 styles 對象時會導致模組化 class 名稱丟失。例如:

MDX 支持

由於 Vite 的底層使用 Rollup,我們可以使用官方的 @mdx-js/rollup 插件來支持 MDX 及其插件。配置如下所示:

remarkGfm 插件用於支持 GitHub 風格的 Markdown,rehypeMdxCodeProps 插件則用於將 props 傳給 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 壓縮。配置如下:

手動 chunks

Vite(或底層的 Rollup)的其中一個優勢是手動 chunks。雖然 React.lazy 用於延後加載組件,但我們可以通過指定手動 chunks 來更多地控制 chunks 以決定哪些組件或模組應該一起打包。

例如,我們可以首先使用 vite-bundle-visualizer 來分析打包大小和依賴關係。然後我們可以編寫一個合適的函數來分割 chunks:

開發伺服器

與生產構建不同,Vite 在開發模式下不會打包原始碼(包括相同 monorepo 中的鏈接依賴項),而是將每個模組視作一個文件。對我們來說,瀏覽器首次加載將加載數百個模塊,這看起來很瘋狂,但實際上在大多數情況下是沒問題的。你可以在這裡看到相關討論。

如果對你來說這是一個問題,那麼一個可替代但不完美的解決方案是將鏈接的依賴項列在 vite.config.tsoptimizeDeps 選項中:

這將「預打包」鏈接的依賴項,從而使開發伺服器更快。但缺點是對於鏈接依賴的 HMR 可能無法如預期的那樣工作。

另外,我們使用了代理,它在生產環境中服務靜態文件並在開發環境中將請求代理到 Vitest 伺服器。為了避免衝突,我們配置了一些特定的端口,簡單易用地配置在 vite.config.ts

環境變數

與 Parcel 不同,Vite 使用現代方法處理環境變數,即使用 import.meta.env。它會自動加載 .env 文件並替換代碼中的變數。但是,這要求所有環境變數必須以 VITE_ 為前綴(可配置)。

當我們使用 Parcel 時,簡單地替換 process.env 變數而不檢查前綴。因此,我們想出了一個使用 define 欄來進行過渡的變通方法:

這使得我們可以逐步為環境變數添加前綴並移除 define 欄。

結論

就是這樣!我們已經成功將三個前端專案從 Parcel 遷移到了 Vite,希望這個短篇故事能對你的遷移有幫助。以下是最終的配置樣子: