為什么 Vue 源碼以及生態(tài)倉庫要遷移 pnpm?
前言
隨著前段時間尤大在 vue3 以及 vite 倉庫中切換包管理為 pnpm 的 pr 成功 merge,以及 vue 生態(tài)中的一些項目例如 VueUse 也切換使用 pnpm,宣告著 vue 生態(tài)中項目倉庫完成了從原有的?yarn workspace monorepo?到?pnpm workspace monorepo?的遷移。

可以看到 vite 核心貢獻(xiàn)者以及 vue 團(tuán)隊成員之一的 patak (https://github.com/patak-js) 在 twitter 上對這次項目遷移的生動描述:“項目如同多米諾骨牌一樣倒向了 pnpm”。
具體關(guān)于 pnpm 相關(guān)介紹可以參考筆者之前寫的一篇文章:?pnpm: 最先進(jìn)的包管理工具?,本文中不會對此做更多的介紹。
vue 遷移項目
其中關(guān)于 vue 生態(tài)中項目遷移的具體過程可以參考這些的一些 pr:
https://github.com/vuejs/vue-next/pull/4766
https://github.com/vitejs/vite/pull/5060
https://github.com/vueuse/vueuse/commit/826351ba1d9c514e34426c85f3d69fb9875c7dd9
其中包括目前 vue3.0 項目源碼倉庫:

以及目前社區(qū)里面火熱的 bundleless 工具 vite 源碼倉庫:

可以看到這兩個的遷移 pr 都是由尤大親手完成改造,同時 pnpm 作者的本人 zkochan(github:?https://github.com/zkochan) 也親自幫 vite 遷移的 pr 做了?code review。
以上幾個項目都是基于 monorepo 來做的倉庫管理,pnpm 的 workspace 在 monorepo 場景下是有著極好的支持,當(dāng)然也有非 monorepo 項目的遷移,例如由筆者遷移的?naive-ui?倉庫的項目中包管理工具為 pnpm 用于提升 CI 下依賴安裝速度的提升(參考pr: https://github.com/TuSimple/naive-ui/pull/1425 )。

下面來介紹一些這次的遷移動機(jī)以及引發(fā)問題的源頭,注意以下的內(nèi)容都是根據(jù)一些社區(qū)討論進(jìn)行推斷的,可能并不完整或者準(zhǔn)確,但是對于具體的細(xì)節(jié)我會盡量還原到位,同時也不會對此過度解讀,希望讀者自行甄別。如果錯誤,歡迎指正。
遷移原因
在尤大9月份的一條 twitter 中,發(fā)起了一條關(guān)于包管理器的投票,當(dāng)時筆者正混跡 pnpm 社區(qū),對此也有所耳聞,具體的投票結(jié)果可以參考:

pnpm 作者本人 zkochan 對此結(jié)果還是很滿意的,因為之前統(tǒng)計社區(qū)的一些趨勢,pnpm 并沒有達(dá)到過如此高的使用率。
隨著該條 twitter 之后,尤大又更新了一條 twitter (這里直接貼原文的內(nèi)容):
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 :/
翻譯過來的內(nèi)容大概是 esbuild 在 v0.13 之后使用了optionalDependencies?來安裝某些不同平臺的依賴(相關(guān) pr 可以參考: https://github.com/evanw/esbuild/pull/1621)。但??yarn 1/2?并不會根據(jù)對應(yīng)的?optional?規(guī)則去下載對應(yīng)平臺的包而是會去選擇下載所有的包。
那么為什么 esbuild 的一個調(diào)整會對尤大產(chǎn)生這樣的念頭呢,因為 vite 目前會在一些場景下使用到 esbuild 這個庫:例如目前開發(fā)階段 vite 會使用 esbuild 進(jìn)行依賴預(yù)打包,來將第三方依賴轉(zhuǎn)成 ESM 格式的 bundle 產(chǎn)物。
這樣的關(guān)系使得 esbuild 作為了 vite 的一個底層依賴,前面也提到過 vite 本身倉庫是基于 yarn workspace monorepo 搭建的,因此每次在開發(fā) vite 時使用 yarn 安裝依賴的過程中,都會去安裝 esbuild 以及相關(guān)的包。
下面筆者會詳細(xì)介紹一下 esbuild 的這個改動的原理以及為什么這個改動會使得 vite 將原有的 monorepo 架子直接做了遷移。
依賴分發(fā)機(jī)制
在上一節(jié)中提到了?esbuild?使用?optionalDependenceis?來作為目前的依賴安裝策略,這節(jié)來介紹一下像這樣這些跨平臺的包依賴分發(fā)的過程。
其實關(guān)于這部分,具體可以參考社區(qū)中這篇:?用 Rust 和 N-API 開發(fā)高性能 Node.js 擴(kuò)展(文章地址: https://zhuanlan.zhihu.com/p/234914336)?文章最開頭的一部分內(nèi)容。
這里筆者以 nodejs 原生拓展(native addon)的代碼分發(fā)方式為例子做個介紹:
關(guān)于 nodejs 拓展開發(fā)可以參考筆者之前寫過的一篇文章:?Nodejs 的 C++ 拓展開發(fā)。
其中主流的分發(fā)方式大概有這樣兩種:
分發(fā) JS 代碼,postinstall 去下載對應(yīng)產(chǎn)物
一般使用其他語言開發(fā)的 addon 之類的會把產(chǎn)物打包成一個可執(zhí)行的二進(jìn)制文件(例如 C++ 拓展一般是?.node?結(jié)尾的文件)。
postinstall 腳本安裝的方式其實在社區(qū)中也是比較常見的,例如安裝 node-sass 就會按照這樣的模式進(jìn)行:

node-sass?會把?native addon(C++ 開發(fā)) 的預(yù)編譯產(chǎn)物放在一個 CDN 地址里面,然后用戶在使用?npm install?安裝?node-sass?的時候,會通過?postinstall?腳本將?addon?產(chǎn)物文件從 CDN 上下載下來。
包括?v0.13?版本之前的?esbuild?其實也是采用這種方式來進(jìn)行分發(fā)。
這種方式其實有個缺點,可以看到圖中下載的二進(jìn)制文件地址是個?Github release?地址,這種情況下常常會因為無法兼顧國內(nèi)/海外用戶。不過一般可以通過在國內(nèi)搭建一個相關(guān)的下載鏡像來解決這個問題,但鏡像不同步的問題也是時常會發(fā)生的。
不同平臺的?native addon?通過不同的 npm 包去分發(fā)
目前市面上很火的兩個構(gòu)建工具,swc?和?esbuild?就采用的這種方式。每一個?native addon?對應(yīng)一個?npm?包。然后將所有的?native addon?對應(yīng)的?npm package?作為?optionalDependencies, 并在這些?npm package?的?package.json?中的?os?以及?cpu?字段,讓對應(yīng)的包管理工具在安裝的時候?qū)Σ煌脚_的包自動選擇去安裝哪個?native package,例如?esbuild?目前的 npm 包結(jié)構(gòu):
{
"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"
}
}
例如其中對應(yīng)的?esbuild-android-arm64?一個安卓平臺的包,以及 arm64 架構(gòu)的包的?package.json?內(nèi)容為:
{
"name": "esbuild-android-arm64",
"version": "0.14.1",
"os": ["android"],
"cpu": ["arm64"]
}
這種方式可以認(rèn)為是目前對使用?native addon?用戶影響最小的分發(fā)方式,包括這里提到的?esbuild、swc?以及?napi-rs?都是采用的這種方式。
這種方式存在的缺點可能就是對開發(fā)者的負(fù)擔(dān)會比較大:因為需要同時維護(hù)多個系統(tǒng)以及 CPU 架構(gòu)的包。同時開發(fā)/調(diào)試也需要消耗很大的工作量。
前面提到的 vite 底層的依賴項?esbuild?在?v0.13?之后由?postinstall script?安裝的方式遷移到了這種?optionalDependencies?的方式:

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

舉個例子來說(該例子來自于?esbuild?作者 evanw 解釋):
以目前?pnpm v6.14?的行為來說,對于?esbuild?下的?optionalDep?進(jìn)行依賴安裝的時候,下面有各種跨平臺以及?cpu?架構(gòu)的包,但實際上只會對符合當(dāng)前平臺架構(gòu)的包進(jìn)行實際的依賴安裝,其他非這一類的包只是會生成一個?meta data?的數(shù)據(jù)在?lock?文件上。
實際上包的體積時遠(yuǎn)遠(yuǎn)大于這份數(shù)據(jù)的大小(例如數(shù)據(jù)約?0.5MB,一個包大約?8MB),那么假設(shè)?optionalDep?下面存在 13 個?package,那么 pnpm 大概會安裝約?0.5mb * 13 + 8mb = 14.5mb?體積的包,而?yarnv1?則會安裝約?0.5mb * 12 + 8mb * 12 = 102mb?的包,這樣會使得因為包管理工具不同的情況下,yarn?安裝的東西比?pnpm?遠(yuǎn)多,從而導(dǎo)致依賴安裝的時間會很慢。
關(guān)于上面提到的依賴安裝問題可以參考下表中的?Downloads extra data?這一欄,可以看到目前 yarn 只有?yarnv3.1.0?這個版本修復(fù)了該問題,pnpm、npm v7?以及?[email protected]?都解決了這個該問題。

vue 生態(tài)項目完成遷移
尤大在社區(qū)里面參考了一些開發(fā)者的意見以及發(fā)起了一個關(guān)于包管理器的投票,twitter?下?90%?左右的回復(fù)都推薦了?pnpm,包括目前?vue core team?的?antfu(https://github.com/antfu) 也已經(jīng)在自己的開源項目?slidev(https://github.com/slidevjs/slidev) 中實踐使用了?pnpm,同時也對?pnpm?的一些功能贊不絕口。

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

在遷移過程中雖然遇到了一些問題,但基本上隨著 pnpm 作者以及社區(qū)的幫助努力下,最后也都成功完成了,實際上的遷移成本也沒有特別的大,可以參考前面的 pr。
在?vite?完成遷移之后,其他的?vue?生態(tài)項目也緊隨其后,雖然這些其他的項目底層可能沒有像?vite?遇到的?esbuild?的問題那樣,但?pnpm?的一些其他優(yōu)勢(例如對依賴的嚴(yán)格管理,快速的依賴安裝,天然的?monoreo workspace?支持等)也吸引著?vue?生態(tài)遷移了包管理工具。因為有了?vite?遷移的經(jīng)驗,其他項目的遷移也都很快完成了,基本上?vue3?的一個遷移相關(guān)的 mr 在一天的時間內(nèi)就完成了合并,慢慢地幾乎?vue?生態(tài)里面大部分項目都完成了遷移。
遷移 pnpm 的實踐
如果想了解如何從一個完整的?yarn workspace?項目遷移到?pnpm workspace,其實也不用去專門研究?vite?或者?vue3?的 pr 是怎么遷移的,在 pnpm 官網(wǎng)上有一篇來自于社區(qū)的文章: Replacing Lerna + Yarn with PNPM Workspaces (地址: https://www.raulmelo.dev/blog/replacing-lerna-and-yarn-with-pnpm-workspaces)。
作者算是比較詳細(xì)的介紹了如果從yarn workspace(項目基于?lerna,但區(qū)別其實不大),遷移到?pnpm workspace?需要做的文件改動以及項目變更。大概是這樣的一個流程:
替換掉腳本命令,與?
yarn?相關(guān)的命令替換為:?pnpm?或者?pnpm run刪除掉頂部?
package.json?中的?yarn workspace?配置替換掉的
?workspace?配置用?pnpm-workspace.yaml?文件替代調(diào)整?
pipeline、以及?Dockfile?或者其他?CI/CD?配置文件里面的依賴安裝命令刪除掉?
yarn.lock?文件(這里也可以使用筆者開發(fā)完善的?pnpm import?命令來完成?yarn.lock?文件轉(zhuǎn)換 /笑 )調(diào)整構(gòu)建相關(guān)的腳本(如果有?
lerna?相關(guān)的?build?腳本)添加一個?
.npmrc?文件用于自定義一些?pnpm?的?CLI?行為表現(xiàn)(也可以不用)
感興趣的同學(xué)可以去參考一下,或者直接和筆者進(jìn)行交流也可以(筆者在字節(jié)也遷移過比較多這一類型的項目,對此也有一些經(jīng)驗,這里就不做過多的介紹了)。
總結(jié)
其實之前在尤大發(fā)起關(guān)于包管理工具的投票時,筆者就已經(jīng)注意到了,同時也關(guān)注到了 vue 開始了 pnpm 的遷移,但當(dāng)時并沒有去仔細(xì)關(guān)注底層的原因。
之前?yarn?的作者發(fā)布了?yarnv3.1(文檔見:?https://dev.to/arcanis/yarn-31-corepack-esm-pnpm-optional-packages--3hak),? 里面最吸引人注意的?feature? 可能是:?yarn?在這個版本下支持了?pnpm?模式的依賴安裝方式(即?content-addressable store),但?yarn?的作者表示這次版本發(fā)布中實現(xiàn)的最復(fù)雜的?feature?是支持了本文中提到的按需安裝不同平臺以及 cpu 架構(gòu)的依賴包:

筆者在最近學(xué)習(xí) swc 的時候,注意到了這一點,抱著刨根問底的心態(tài),去研究了一下這一系列遷移問題背后的原因(原因令人暖心)。
最后寫了這樣一篇干貨性不是很強(qiáng)的文章,可以作為一個記錄如果之后有相關(guān)的需求進(jìn)行開發(fā)(例如使用其他語言開發(fā)?native addon?的時候)的話。同時也希望 pnpm 未來能成為一個社區(qū)中流行的包管理工具吧~
另外,推介一本不錯的書 ???《Nodejs入門指南》,書用純小白文字講透Node.js 核心框架, 在Node.js中與數(shù)據(jù)庫MongoDB進(jìn)行交互,用體系化前后端框架實現(xiàn)項目分離,層層抽絲剝繭,幫助依次突破學(xué)習(xí)障礙。
最后
如果你覺得這篇內(nèi)容對你挺有啟發(fā),我想邀請你幫我三個小忙:
點個「在看」,讓更多的人也能看到這篇內(nèi)容(喜歡不點在看,都是耍流氓 -_-)
歡迎加我微信「qianyu443033099」拉你進(jìn)技術(shù)群,長期交流學(xué)習(xí)...
關(guān)注公眾號「前端下午茶」,持續(xù)為你推送精選好文,也可以加我為好友,隨時聊騷。

