深入淺出 npm - yarn - pnpm 包管理機制
一起進步🚀)" data-from="0" has-insert-preloading="1" data-darkmode-color-16565923456148="rgb(163, 163, 163)" data-darkmode-original-color-16565923456148="#fff|rgb(63, 63, 63)">
前端開發者們每天都在接觸 xxx install,包管理器是必不可少的工具。
本文會以儘量簡潔的語言來描述當下主流包管理工具 npm、yarn、pnpm 的管理策略以及進化史,不涉及任何晦澀的代碼。
npm - 先鋒
2010 年 1 月,一款名爲 npm 的包管理器誕生。
很多人認爲 npm 是 node package manager 的縮寫,其實不是,而且 npm 根本也不是任何短語的縮寫。
npm 官方闢謠:
它的前身其實是名爲 pm(pkgmakeinst) 的 bash 工具,它可以在各種平臺上安裝各種東西。
硬要說縮寫的話,也應該是 node pm 或者 new pm。
嵌套的 node_modules 結構
npm 在早期採用的是嵌套的 node_modules 結構,直接依賴會平鋪在 node_modules 下,子依賴嵌套在直接依賴的 node_modules 中。
比如項目依賴了 A 和 C,而 A 和 C 依賴了不同版本的 B@1.0 和 B@2.0,node_modules 結構如下:
node_modules
├── A@1.0.0
│ └── node_modules
│ └── B@1.0.0
└── C@1.0.0
└── node_modules
└── B@2.0.0
如果 D 也依賴 B@1.0,會生成如下的嵌套結構:
node_modules
├── A@1.0.0
│ └── node_modules
│ └── B@1.0.0
├── C@1.0.0
│ └── node_modules
│ └── B@2.0.0
└── D@1.0.0
└── node_modules
└── B@1.0.0
可以看到同版本的 B 分別被 A 和 D 安裝了兩次。
依賴地獄 Dependency Hell
在真實場景下,依賴增多,冗餘的包也變多,node_modules 最終會堪比黑洞,很快就能把磁盤佔滿。而且依賴嵌套的深度也會十分可怕,這個就是依賴地獄。
扁平的 node_modules 結構
爲了將嵌套的依賴儘量打平,避免過深的依賴樹和包冗餘,npm v3 將子依賴「提升」(hoist),採用扁平的 node_modules 結構,子依賴會盡量平鋪安裝在主依賴項所在的目錄中。
node_modules
├── A@1.0.0
├── B@1.0.0
└── C@1.0.0
└── node_modules
└── B@2.0.0
可以看到 A 的子依賴的 B@1.0 不再放在 A 的 node_modules 下了,而是與 A 同層級。
而 C 依賴的 B@2.0 因爲版本號原因還是嵌套在 C 的 node_modules 下。
這樣不會造成大量包的重複安裝,依賴的層級也不會太深,解決了依賴地獄問題,但也形成了新的問題。
幽靈依賴 Phantom dependencies
幽靈依賴是指在 package.json 中未定義的依賴,但項目中依然可以正確地被引用到。
比如上方的示例其實我們只安裝了 A 和 C:
{
"dependencies": {
"A": "^1.0.0",
"C": "^1.0.0"
}
}
由於 B 在安裝時被提升到了和 A 同樣的層級,所以在項目中引用 B 還是能正常工作的。
幽靈依賴是由依賴的聲明丟失造成的,如果某天某個版本的 A 依賴不再依賴 B 或者 B 的版本發生了變化,那麼就會造成依賴缺失或兼容性問題。
不確定性 Non-Determinism
不確定性是指:同樣的 package.json 文件,install 依賴後可能不會得到同樣的 node_modules 目錄結構。
還是之前的例子,A 依賴 B@1.0,C 依賴 B@2.0,依賴安裝後究竟應該提升 B 的 1.0 還是 2.0。
node_modules
├── A@1.0.0
├── B@1.0.0
└── C@1.0.0
└── node_modules
└── B@2.0.0
node_modules
├── A@1.0.0
│ └── node_modules
│ └── B@1.0.0
├── B@2.0.0
└── C@1.0.0
取決於用戶的安裝順序。
如果有 package.json 變更,本地需要刪除 node_modules 重新 install,否則可能會導致生產環境與開發環境 node_modules 結構不同,代碼無法正常運行。
依賴分身 Doppelgangers
假設繼續再安裝依賴 B@1.0 的 D 模塊和依賴 @B2.0 的 E 模塊,此時:
-
A 和 D 依賴 B@1.0
-
C 和 E 依賴 B@2.0
以下是提升 B@1.0 的 node_modules 結構:
node_modules
├── A@1.0.0
├── B@1.0.0
├── D@1.0.0
├── C@1.0.0
│ └── node_modules
│ └── B@2.0.0
└── E@1.0.0
└── node_modules
└── B@2.0.0
可以看到 B@2.0 會被安裝兩次,實際上無論提升 B@1.0 還是 B@2.0,都會存在重複版本的 B 被安裝,這兩個重複安裝的 B 就叫 doppelgangers。
而且雖然看起來模塊 C 和 E 都依賴 B@2.0,但其實引用的不是同一個 B,假設 B 在導出之前做了一些緩存或者副作用,那麼使用者的項目就會因此而出錯。
yarn - 創新
提升安裝速度
在 npm 中安裝依賴時,安裝任務是串行的,會按包順序逐個執行安裝,這意味着它會等待一個包完全安裝,然後再繼續下一個。
爲了加快包安裝速度,yarn 採用了並行操作,在性能上有顯著的提高。而且在緩存機制上,yarn 會將每個包緩存在磁盤上,在下一次安裝這個包時,可以脫離網絡實現從磁盤離線安裝。
lockfile 解決不確定性
yarn 更大的貢獻是發明了 yarn.lock。
在依賴安裝時,會根據 package.josn 生成一份 yarn.lock 文件。
lockfile 裏記錄了依賴,以及依賴的子依賴,依賴的版本,獲取地址與驗證模塊完整性的 hash。
即使是不同的安裝順序,相同的依賴關係在任何的環境和容器中,都能得到穩定的 node_modules 目錄結構,保證了依賴安裝的確定性。
所以 yarn 在出現時被定義爲快速、安全、可靠的依賴管理。而 npm 在一年後的 v5 才發佈了 package-lock.json。
與 npm 一樣的弊端
yarn 依然和 npm 一樣是扁平化的 node_modules 結構,沒有解決幽靈依賴和依賴分身問題。
pnpm - 後浪
pnpm - performant npm,在 2017 年正式發佈,定義爲快速的,節省磁盤空間的包管理工具,開創了一套新的依賴管理機制,成爲了包管理的後起之秀。
內容尋址存儲 CAS
與依賴提升和扁平化的 node_modules 不同,pnpm 引入了另一套依賴管理策略:內容尋址存儲。
該策略會將包安裝在系統的全局 store 中,依賴的每個版本只會在系統中安裝一次。
在引用項目 node_modules 的依賴時,會通過硬鏈接與符號鏈接在全局 store 中找到這個文件。爲了實現此過程,node_modules 下會多出 .pnpm
目錄,而且是非扁平化結構。
-
硬鏈接 Hard link:硬鏈接可以理解爲源文件的副本,項目裏安裝的其實是副本,它使得用戶可以通過路徑引用查找到全局 store 中的源文件,而且這個副本根本不佔任何空間。同時,pnpm 會在全局 store 裏存儲硬鏈接,不同的項目可以從全局 store 尋找到同一個依賴,大大地節省了磁盤空間。
-
符號鏈接 Symbolic link:也叫軟連接,可以理解爲快捷方式,pnpm 可以通過它找到對應磁盤目錄下的依賴地址。
還是使用上面 A,B,C 模塊的示例,使用 pnpm 安裝依賴後 node_modules 結構如下:
node_modules
├── .pnpm
│ ├── A@1.0.0
│ │ └── node_modules
│ │ ├── A => <store>/A@1.0.0
│ │ └── B => ../../B@1.0.0
│ ├── B@1.0.0
│ │ └── node_modules
│ │ └── B => <store>/B@1.0.0
│ ├── B@2.0.0
│ │ └── node_modules
│ │ └── B => <store>/B@2.0.0
│ └── C@1.0.0
│ └── node_modules
│ ├── C => <store>/C@1.0.0
│ └── B => ../../B@2.0.0
│
├── A => .pnpm/A@1.0.0/node_modules/A
└── C => .pnpm/C@1.0.0/node_modules/C
<store>/xxx
開頭的路徑是硬鏈接,指向全局 store 中安裝的依賴。
其餘的是符號鏈接,指向依賴的快捷方式。
pnpm 官方圖片也清晰地解釋了這套機制:
未來可期
這套全新的機制設計地十分巧妙,不僅兼容 node 的依賴解析,同時也解決了:
-
幽靈依賴問題:只有直接依賴會平鋪在 node_modules 下,子依賴不會被提升,不會產生幽靈依賴。
-
依賴分身問題:相同的依賴只會在全局 store 中安裝一次。項目中的都是源文件的副本,幾乎不佔用任何空間,沒有了依賴分身。
同時,由於鏈接的優勢,pnpm 的安裝速度在大多數場景都比 npm 和 yarn 快 2 倍,節省的磁盤空間也更多。
但也存在一些弊端:
-
由於 pnpm 創建的 node_modules 依賴軟鏈接,因此在不支持軟鏈接的環境中,無法使用 pnpm,比如 Electron 應用。
-
因爲依賴源文件是安裝在 store 中,調試依賴或 patch-package 給依賴打補丁也不太方便,可能會影響其他項目。
yarn Plug’n’Play - 探索
npm 與 yarn 的依賴安裝與依賴解析都涉及大量的文件 I/O,效率不高。開發 Plug’n’Play 最直接的原因就是依賴引用慢,依賴安裝慢。
拋棄 node_modules
無論是 npm 還是 yarn,都具備緩存的功能,大多數情況下安裝依賴時,其實是將緩存中的相關包複製到項目目錄中 node_modules 裏。
而 yarn PnP 則不會進行拷貝這一步,而是在項目裏維護一張靜態映射表 pnp.cjs。
pnp.cjs 會記錄依賴在緩存中的具體位置,所有依賴都存在全局緩存中。同時自建了一個解析器,在依賴引用時,幫助 node 從全局緩存目錄中發現依賴,而不是查找 node_modules。
這樣就避免了大量的 I/O 操作同時項目目錄也不會有 node_modules 目錄生成,同版本的依賴在全局也只會有一份,依賴的安裝速度和解析速度都有較大提升。
pnpm 在 2020 年底的 v5.9 也支持了 PnP。
脫離 node 生態
pnp 比較明顯的缺點是脫離了 node 生態。
-
因爲使用 PnP 不會再有 node_modules 了,但是 Webpack,Babel 等各種前端工具都依賴 node_modules。雖然很多工具比如 pnp-webpack-plugin 已經在解決了,但難免會有兼容性風險。
-
PnP 自建了依賴解析器,所有的依賴引用都必須由解析器執行,因此只能通過 yarn 命令來執行 node 腳本。
總結
目前還沒有完美的依賴管理方案,可以看到在依賴管理的發展過程中,出現了:
-
不同的 node_modules 結構,有嵌套,扁平,甚至沒有 node_modules,不同的結構也伴隨着兼容與安全問題。
-
不同的依賴存儲方式來節約磁盤空間,提升安裝速度。
-
每種管理器都伴隨新的工具和命令,不同程度的可配置性和擴展性,影響開發者體驗。
-
這些包管理器也對 monorepo 有不同程度的支持,會直接影響項目的可維護性和速度。
庫與開發者能夠在這樣優化與創新的發展過程中互相學習,站在巨人的肩膀上繼續前進,不斷推動前端工程領域的發展。
參考
-
https://mp.weixin.qq.com/s/CYQQKvy9MaGHdpSHpBJwzw by 王辰新 (朔宸)
-
https://blog.logrocket.com/javascript-package-managers-compared/ by Sebastian Weber
-
https://wxsm.space/2021/npm-history/ by wxsm
-
https://loveky.github.io/2019/02/11/yarn-pnp/ by LOVEKY
-
https://juejin.cn/post/7097906848505806885 by 高尚的尚
-
https://juejin.cn/post/7001794162970361892 by zoomdong
-
https://juejin.cn/post/6932046455733485575 by 神三元
-
https://juejin.cn/post/6844903814038831118 by 荒山
-
https://juejin.cn/post/7060844948316225572 by 酒窩 yun 過去了
-
https://zhuanlan.zhihu.com/p/353208988 by 松若章
-
https://segmentfault.com/a/1190000017075256 by caoweiju
-
https://segmentfault.com/a/1190000009709213 by meikidd
-
https://pnpm.io/zh/symlinked-node-modules-structure
-
https://pnpm.io/zh/blog/2020/05/27/flat-node-modules-is-not-the-only-way
一起進步🚀)" data-from="0" has-insert-preloading="1" data-darkmode-color-16565923456148="rgb(163, 163, 163)" data-darkmode-original-color-16565923456148="#fff|rgb(63, 63, 63)"> 小李的前端小屋 關注我 ().then(() => 一起進步🚀)
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/ZTI-8RI0l314Ki9oBxqRWw