爲什麼 Vue 源碼及生態倉庫要遷移 pnpm?

前言

隨着前段時間尤大在 vue3 以及 vite 倉庫中切換包管理爲 pnpm 的 pr 成功 merge,以及 vue 生態中的一些項目例如 VueUse 也切換使用 pnpm,宣告着 vue 生態中項目倉庫完成了從原有的 yarn workspace monorepo 到 pnpm workspace monorepo 的遷移。

可以看到 vite 核心貢獻者以及 vue 團隊成員之一的 patak (https://github.com/patak-js) 在 twitter 上對這次項目遷移的生動描述:“項目如同多米諾骨牌一樣倒向了 pnpm”。

具體關於 pnpm 相關介紹可以參考筆者之前寫的一篇文章: pnpm: 最先進的包管理工具 ,本文中不會對此做更多的介紹。

vue 遷移項目

其中關於 vue 生態中項目遷移的具體過程可以參考這些的一些 pr:

其中包括目前 vue3.0 項目源碼倉庫:

以及目前社區裏面火熱的 bundleless 工具 vite 源碼倉庫:

可以看到這兩個的遷移 pr 都是由尤大親手完成改造,同時 pnpm 作者的本人 zkochan(github: https://github.com/zkochan) 也親自幫 vite 遷移的 pr 做了 code review

以上幾個項目都是基於 monorepo 來做的倉庫管理,pnpm 的 workspace 在 monorepo 場景下是有着極好的支持,當然也有非 monorepo 項目的遷移,例如由筆者遷移的 naive-ui 倉庫的項目中包管理工具爲 pnpm 用於提升 CI 下依賴安裝速度的提升 (參考 pr: https://github.com/TuSimple/naive-ui/pull/1425)。

下面來介紹一些這次的遷移動機以及引發問題的源頭,注意以下的內容都是根據一些社區討論進行推斷的,可能並不完整或者準確,但是對於具體的細節我會盡量還原到位,同時也不會對此過度解讀,希望讀者自行甄別。如果錯誤,歡迎指正。

遷移原因

在尤大 9 月份的一條 twitter 中,發起了一條關於包管理器的投票,當時筆者正混跡 pnpm 社區,對此也有所耳聞,具體的投票結果可以參考:

pnpm 作者本人 zkochan 對此結果還是很滿意的,因爲之前統計社區的一些趨勢,pnpm 並沒有達到過如此高的使用率。

隨着該條 twitter 之後,尤大又更新了一條 twitter (這裏直接貼原文的內容):

esbuild 0.13 now uses optionalDependencies to install platform-specific binaries. Yarn 1/2 will download all binaries before picking the right one. Other (update to date) package managers only downloads the matching one.

This may be the thing that pushes me away from Yarn 1 :/

翻譯過來的內容大概是 esbuild 在 v0.13 之後使用了optionalDependencies 來安裝某些不同平臺的依賴 (相關 pr 可以參考: https://github.com/evanw/esbuild/pull/1621)。但  yarn 1/2 並不會根據對應的 optional 規則去下載對應平臺的包而是會去選擇下載所有的包。

那麼爲什麼 esbuild 的一個調整會對尤大產生這樣的念頭呢,因爲 vite 目前會在一些場景下使用到 esbuild 這個庫:例如目前開發階段 vite 會使用 esbuild 進行依賴預打包,來將第三方依賴轉成 ESM 格式的 bundle 產物。

這樣的關係使得 esbuild 作爲了 vite 的一個底層依賴,前面也提到過 vite 本身倉庫是基於 yarn workspace monorepo 搭建的,因此每次在開發 vite 時使用 yarn 安裝依賴的過程中,都會去安裝 esbuild 以及相關的包。

下面筆者會詳細介紹一下 esbuild 的這個改動的原理以及爲什麼這個改動會使得 vite 將原有的 monorepo 架子直接做了遷移。

依賴分發機制

在上一節中提到了 esbuild 使用 optionalDependenceis 來作爲目前的依賴安裝策略,這節來介紹一下像這樣這些跨平臺的包依賴分發的過程。

其實關於這部分,具體可以參考社區中這篇: 用 Rust 和 N-API 開發高性能 Node.js 擴展 (文章地址: https://zhuanlan.zhihu.com/p/234914336) 文章最開頭的一部分內容。

這裏筆者以 nodejs 原生拓展 (native addon) 的代碼分發方式爲例子做個介紹:

關於 nodejs 拓展開發可以參考筆者之前寫過的一篇文章: Nodejs 的 C++ 拓展開發

其中主流的分發方式大概有這樣兩種:

分發 JS 代碼,postinstall 去下載對應產物

一般使用其他語言開發的 addon 之類的會把產物打包成一個可執行的二進制文件 (例如 C++ 拓展一般是 .node 結尾的文件)。

postinstall 腳本安裝的方式其實在社區中也是比較常見的,例如安裝 node-sass 就會按照這樣的模式進行:

node-sass 會把 native addon(C++ 開發) 的預編譯產物放在一個 CDN 地址裏面,然後用戶在使用 npm install 安裝 node-sass 的時候,會通過 postinstall 腳本將 addon 產物文件從 CDN 上下載下來。

包括 v0.13 版本之前的 esbuild 其實也是採用這種方式來進行分發。

這種方式其實有個缺點,可以看到圖中下載的二進制文件地址是個 Github release 地址,這種情況下常常會因爲無法兼顧國內 / 海外用戶。不過一般可以通過在國內搭建一個相關的下載鏡像來解決這個問題,但鏡像不同步的問題也是時常會發生的。

不同平臺的 native addon 通過不同的 npm 包去分發

目前市面上很火的兩個構建工具,swc 和 esbuild 就採用的這種方式。每一個 native addon 對應一個 npm 包。然後將所有的 native addon 對應的 npm package 作爲 optionalDependencies, 並在這些 npm package 的 package.json 中的 os 以及 cpu 字段,讓對應的包管理工具在安裝的時候對不同平臺的包自動選擇去安裝哪個 native package,例如 esbuild 目前的 npm 包結構:

{
  "name": "esbuild",
  "version": "0.14.1",
  "optionalDependencies": {
    "esbuild-android-arm64": "0.14.1",
    "esbuild-darwin-64": "0.14.1",
    "esbuild-darwin-arm64": "0.14.1",
    "esbuild-freebsd-64": "0.14.1",
    "esbuild-freebsd-arm64": "0.14.1",
    "esbuild-linux-32": "0.14.1",
    "esbuild-linux-64": "0.14.1",
    "esbuild-linux-arm": "0.14.1",
    "esbuild-linux-arm64": "0.14.1",
    "esbuild-linux-mips64le": "0.14.1",
    "esbuild-linux-ppc64le": "0.14.1",
    "esbuild-netbsd-64": "0.14.1",
    "esbuild-openbsd-64": "0.14.1",
    "esbuild-sunos-64": "0.14.1",
    "esbuild-windows-32": "0.14.1",
    "esbuild-windows-64": "0.14.1",
    "esbuild-windows-arm64": "0.14.1"
  }
}

例如其中對應的 esbuild-android-arm64 一個安卓平臺的包,以及 arm64 架構的包的 package.json 內容爲:

{
  "name": "esbuild-android-arm64",
  "version": "0.14.1",
  "os": ["android"],
  "cpu": ["arm64"]
}

這種方式可以認爲是目前對使用 native addon 用戶影響最小的分發方式,包括這裏提到的 esbuildswc 以及 napi-rs 都是採用的這種方式。

這種方式存在的缺點可能就是對開發者的負擔會比較大:因爲需要同時維護多個系統以及 CPU 架構的包。同時開發 / 調試也需要消耗很大的工作量。

前面提到的 vite 底層的依賴項 esbuild 在 v0.13 之後由 postinstall script 安裝的方式遷移到了這種 optionalDependencies 的方式:

包管理器支持

在上一節中我們介紹到了 native addon 的一些常見的依賴分發機制,同時也介紹到了 esbuild 目前在 v0.13 之後採用了 optionalDependencies 機制。前面也有提到因爲 yarn1 的依賴安裝機制問題導致在 vite 進行開發時,每次都會下載 esbuild 中所有的跨平臺包 (例如在 android 平臺上也會下載 ios 的包),對此會導致每次給 vite 倉庫進行依賴安裝的時候,耗費很久的時間。參考 yarn 下面的 issue(https://github.com/yarnpkg/berry/issues/3317)。

舉個例子來說 (該例子來自於 esbuild 作者 evanw 解釋):

以目前 pnpm v6.14 的行爲來說,對於 esbuild 下的 optionalDep 進行依賴安裝的時候,下面有各種跨平臺以及 cpu 架構的包,但實際上只會對符合當前平臺架構的包進行實際的依賴安裝,其他非這一類的包只是會生成一個 meta data 的數據在 lock 文件上。

實際上包的體積時遠遠大於這份數據的大小 (例如數據約 0.5MB,一個包大約 8MB),那麼假設 optionalDep 下面存在 13 個 package,那麼 pnpm 大概會安裝約 0.5mb * 13 + 8mb = 14.5mb 體積的包,而 yarnv1 則會安裝約 0.5mb * 12 + 8mb * 12 = 102mb 的包,這樣會使得因爲包管理工具不同的情況下,yarn 安裝的東西比 pnpm 遠多,從而導致依賴安裝的時間會很慢。

關於上面提到的依賴安裝問題可以參考下表中的 Downloads extra data 這一欄,可以看到目前 yarn 只有 yarnv3.1.0 這個版本修復了該問題,pnpmnpm v7 以及 cnpm@7.1.0 都解決了這個該問題。

vue 生態項目完成遷移

尤大在社區裏面參考了一些開發者的意見以及發起了一個關於包管理器的投票,twitter 下 90% 左右的回覆都推薦了 pnpm,包括目前 vue core team 的 antfu(https://github.com/antfu) 也已經在自己的開源項目 slidev(https://github.com/slidevjs/slidev) 中實踐使用了 pnpm,同時也對 pnpm 的一些功能讚不絕口。

於是 vite 直接在幾天之後開始了由 yarn workspace 到 pnpm workspace 的遷移:

在遷移過程中雖然遇到了一些問題,但基本上隨着 pnpm 作者以及社區的幫助努力下,最後也都成功完成了,實際上的遷移成本也沒有特別的大,可以參考前面的 pr。

在 vite 完成遷移之後,其他的 vue 生態項目也緊隨其後,雖然這些其他的項目底層可能沒有像 vite 遇到的 esbuild 的問題那樣,但 pnpm 的一些其他優勢 (例如對依賴的嚴格管理,快速的依賴安裝,天然的 monoreo workspace 支持等) 也吸引着 vue 生態遷移了包管理工具。因爲有了 vite 遷移的經驗,其他項目的遷移也都很快完成了,基本上 vue3 的一個遷移相關的 mr 在一天的時間內就完成了合併,慢慢地幾乎 vue 生態裏面大部分項目都完成了遷移。

遷移 pnpm 的實踐

如果想了解如何從一個完整的 yarn workspace 項目遷移到 pnpm workspace,其實也不用去專門研究 vite 或者 vue3 的 pr 是怎麼遷移的,在 pnpm 官網上有一篇來自於社區的文章: Replacing Lerna + Yarn with PNPM Workspaces (地址: https://www.raulmelo.dev/blog/replacing-lerna-and-yarn-with-pnpm-workspaces)。

作者算是比較詳細的介紹瞭如果從yarn workspace(項目基於 lerna,但區別其實不大),遷移到 pnpm workspace 需要做的文件改動以及項目變更。大概是這樣的一個流程:

感興趣的同學可以去參考一下,或者直接和筆者進行交流也可以 (筆者在字節也遷移過比較多這一類型的項目,對此也有一些經驗,這裏就不做過多的介紹了)。

總結

其實之前在尤大發起關於包管理工具的投票時,筆者就已經注意到了,同時也關注到了 vue 開始了 pnpm 的遷移,但當時並沒有去仔細關注底層的原因。

之前 yarn 的作者發佈了 yarnv3.1(文檔見: https://dev.to/arcanis/yarn-31-corepack-esm-pnpm-optional-packages--3hak),  裏面最吸引人注意的 feature  可能是: yarn 在這個版本下支持了 pnpm 模式的依賴安裝方式 (即 content-addressable store),但 yarn 的作者表示這次版本發佈中實現的最複雜的 feature 是支持了本文中提到的按需安裝不同平臺以及 cpu 架構的依賴包:

筆者在最近學習 swc 的時候,注意到了這一點,抱着刨根問底的心態,去研究了一下這一系列遷移問題背後的原因 (原因令人暖心)。

最後寫了這樣一篇乾貨性不是很強的文章,可以作爲一個記錄如果之後有相關的需求進行開發 (例如使用其他語言開發 native addon 的時候) 的話。同時也希望 pnpm 未來能成爲一個社區中流行的包管理工具吧~

公衆號:前端食堂

知乎:童歐巴

掘金:童歐巴

這是一個終身學習的男人,他在堅持自己熱愛的事情,歡迎你加入前端食堂,和這個男人一起開心的變胖~

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/0PfyRfv23aTF2sV_RY11Fw