# pnpm - 套件管理工具小背景 - **先驅者npm** 2010 年1 月發布 2020 年,GitHub 收購了npm,原則上 npm 現在歸微軟管理 - 關於npm冷知識 Q:npm 縮寫是 node package manager? A:npm 不是任何短語的縮寫 > npm 官方闢謠: > ![https://pic1.zhimg.com/80/v2-c6c2262acec612391bd32b9bf2ac9de8_720w.jpg](https://pic1.zhimg.com/80/v2-c6c2262acec612391bd32b9bf2ac9de8_720w.jpg) - **創新者Yarn Classic (V1)** 在2016 年10 月,由Facebook 宣布與Google 和其他公司合作開發 - **Yarn Berry (>V2)** Yarn 2 於2020年1月發布 主要創新是[即插即用(PnP)](https://yarnpkg.com/features/pnp)方法,為修復node_modules的策略 1. 不生成`node_modules` ,生成一個帶有依賴查找表的`.pnp.cjs` 文件(非嵌套結構) 2. 每個package都以**zip 文件**的形式存儲在文件夾內替代`.yarn/cache/` (省磁盤空間) ⇒ 要求維護者更新現有的package ⇒ 許多知名開發人員[公開批評Yarn 2](https://www.youtube.com/watch?v=bPae4Z8BFt8) ⇒ JS生態系統為PnP 提供了越來越多的支持 ⇒ `Yarn Berry` 還很年輕 - **更高效的pnpm** 由Zoltan  Kochan於2017 年發布 > Fast, disk space efficient package manager > performant npm,定義為快速的,節省磁盤空間的packages管理工具 ****1. 速度快**** ****2. 高效率利用磁碟空間**** ****3. 支援monorepo ⇒ 軟體開發策略**** - 簡述 以前後端分離的 web 開發為例,monorepo 就是把前端與後端的原始碼都放在同個 repository;而 polyrepo 則是把前端與後端分為兩個不同的 repository。 ***monorepo*** ```html ├── docs # Documents │ ├── api # API spec │ ├── archived # Archived documents │ └── ... ├── README.md ├── services │ ├── ui │ ├── api │ └── ... ├── packages │ ├── util │ ├── core │ └── ... └── package.json ``` ***polyrepo ⇒* dependencies 的版本管理複雜/重覆配置/共用程式碼維護成本高** ```html ui ├── docs # Documents │ └── ... ├── README.md ├── src │ ├── util │ ├── components │ ├── containers │ └── ... └── package.json api ├── docs # Documents │ └── ... ├── README.md ├── src │ ├── util │ ├── router │ ├── model │ └── ... └── package.json ``` ****4. 安全性高**** ![](https://i.imgur.com/kBhvTEI.png) [https://github.com/pnpm/benchmarks-of-javascript-package-managers](https://github.com/pnpm/benchmarks-of-javascript-package-managers) ## [The State of JS 2021 results : ****monorepo****](https://2021.stateofjs.com/en-US/libraries/monorepo-tools/) ## ****npm/yarn/pmpm**** - 在npm1、npm2中=>**嵌套結構** ```html node_modules ├── A@1.0.0 │ └── node_modules │ └── B@1.0.0 └── C@1.0.0 └── node_modules └── B@2.0.0 ``` - ****依賴地獄Dependency Hell****,文件路徑過長 - 重複的package被安裝,文件體積超大 ```html 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 ``` - 在npm3 、yarn (V1)中=>**扁平化依賴**(將子依賴提升hoist) ```html node_modules ├── A@1.0.0 ├── B@1.0.0 └── C@1.0.0 └── node_modules └── B@2.0.0 ``` - 新問題產生 - 依賴結構的**不確定性(Non-Determinism)** > 不確定性是指:同樣的package.json,安裝後可能得到不同node_modules 的目錄結構。 > Q:A 依賴B@1.0,C 依賴B@2.0,安裝後究竟應該提升B 的1.0 還是C的2.0? ```html 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 ``` A:都有可能,取決於用戶的安裝順序。取決於A 和C 在 `package.json`中的位置,如果A 聲明在前面,那麼就是前面的結構。 ### ****lockfile 解決不確定性**** > 無論是`package-lock.json`(npm 5.x才出現)還是`yarn.lock`,都是為了保證install 之後都產生確定的`node_modules`結構。 > > > lockfile 裡記錄了依賴,以及依賴的子依賴,依賴的版本,獲取地址與驗證模塊完整性的hash。 > > 即使是不同的安裝順序,相同的依賴關係在任何的環境和容器中,都能得到穩定的node_modules 目錄結構,保證了依賴安裝的確定性。 > - ****幽靈依賴Phantom dependencies**** : > 幽靈依賴是指:在package.json 中未定義的依賴,但項目中依然可以正確地被引用到。 > 比如上方的示例其實我們只安裝了A和C ```html { "dependencies": { "A": "^1.0.0", "C": "^1.0.0" } } ``` 由於B 在安裝時被提升到了和A 同樣的層級,所以在專案中引用B 能正常運作。 如果某天某個版本的A 依賴不再依賴B 或者B 的版本有所變化,那麼就會造成依賴缺失問題。 - ****依賴分身Doppelgangers**** ex: 再安裝依賴@B2.0 的D ⇒這兩個重複安裝的B 就叫doppelgangers ```html node_modules ├── A@1.0.0 ├── B@1.0.0 ├── C@1.0.0 │ └── node_modules │ └── B@2.0.0 └── D@1.0.0 └── node_modules └── B@2.0.0 ``` - pnpm =>**非扁平化依賴** (硬鏈接hard link + 符號鏈接symlink ****) **A hard link is actually the same file that it links to but with a different name. ⇒ contents** **A symbolic link is special file that points to another pre-existing file (original file). ⇒ path** ![](https://i.imgur.com/bPWz4JE.png) 將packages安裝在全局store 中,在引用項目node_modules 的依賴時,會通過硬鏈接與符號鏈接在全局store 中找到這個文件。為了實現此過程,**node_modules 下會多出非扁平化結構的 `.pnpm` 目錄**。 - 基於**硬鏈接機制(hard link,**類似副本**)**提升package的安裝速度 > 硬鏈接可以理解為,安裝的其實是副本,它使得用戶可以通過路徑引用查找到全局store 中的源文件。 > - 使用**符號鏈接(symlink,** 類似於windows快捷方式)的非扁平的node_modules結構 > 僅將項目的直接依賴項添加到node_modules 的根目錄下, 通過符號連接查找虛擬磁盤目錄.pnpm下的依賴包 > ex: 安裝了依賴於 `foo@1.0.0` 的`bar@1.0.0`。 ![](https://i.imgur.com/MCL296e.png) (1)pnpm 會將兩個packages硬鏈接到 `node_modules` 如下所示: ```html node_modules └── .pnpm ├── foo@1.0.0 │ └── node_modules │ └── foo -> <store>/foo │ ├── index.js │ └── package.json └── bar@1.0.0 └── node_modules └── bar -> <store>/bar ├── index.js └── package.json ``` > `<store>/xxx`開頭的路徑是硬鏈接,指向全局store 中安裝的依賴。 > > 這是 `node_modules` 中的only "real" files。真正的文件位置 : 都是在`<package-name>@version/node_modules/<package-name>`這種目錄結構中 > > 一旦所有package都硬鏈接到`node_modules`,就會創建symbolic links 來構建嵌套的依賴關係圖結構。 > (2)處理symbolic links依賴 : `foo`將被符號鏈接到 `bar@1.0.0/node_modules` 文件夾: ⇒ bar平級目錄創建foo,foo指向foo @1.0.0底下的foo ```html node_modules └── .pnpm ├── foo @1.0.0 │ └── node_modules │ └── foo -> <store>/foo └── bar@1.0.0 └── node_modules ├── bar -> <store>/bar └── foo -> ../../foo @1.0.0/node_modules/foo ``` (3)處理直接依賴:`bar`將被符號鏈接至根目錄的 `node_modules` 文件夾 ⇒ 頂層node_modules目錄下創建bar,指向bar@1.0.0下的bar。 ```html node_modules ├── bar -> ./.pnpm/bar@1.0.0/node_modules/bar └── .pnpm ├── foo@1.0.0 │ └── node_modules │ └── foo -> <store>/foo └── bar@1.0.0 └── node_modules ├── bar -> <store>/bar └── foo -> ../../foo@1.0.0/node_modules/foo ``` (4)添加 `qar@2.0.0` 作為 `bar` 和 `foo` 的依賴項 ```html node_modules ├── bar -> ./.pnpm/bar@1.0.0/node_modules/bar └── .pnpm ├── foo@1.0.0 │ └── node_modules │ ├── foo -> <store>/foo │ └── qar -> ../../qar@2.0.0/node_modules/qar ├── bar@1.0.0 │ └── node_modules │ ├── bar -> <store>/bar │ ├── foo -> ../../foo@1.0.0/node_modules/foo │ └── qar -> ../../qar@2.0.0/node_modules/qar └── qar@2.0.0 └── node_modules └── qar -> <store>/qar ``` > 無論依賴項的數量和依賴關係圖的深度如何,即使圖形現在更深(`bar >foo > qar`),目錄深度仍然相同。 > 與Node 的模塊解析算法兼容,在解析模塊時,Node會忽略symlinks 執行realpath,因此當 `bar@1.0.0/node_modules/bar/index.js` 需要 foo 時,Node 不會使用在 `bar@1.0.0/node_modules/foo` 的foo,相反,foo是被解析到其實際位置(`foo@1.0.0/node_modules/foo`)。因此,只有真正在依賴項中的package才能訪問。 解決: 1. 幽靈依賴問題:只有直接依賴會平鋪在node_modules 下,子依賴不會被提升,不會產生幽靈依賴。 2. 依賴分身問題:packages被存儲在一個全局的store目錄下,不同的項目依賴同一個package(版本相同)時,會硬鏈接至此位置,無需重新安裝。 ## 回到最初的ex: if A 依賴B@1.0,C 依賴B@2.0 ```html 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 ``` (1)pnpm 會將all packages硬鏈接到 `node_modules` (2)處理symbolic links依賴 : B 將被符號鏈接到 `A@1.0.0/node_modules` 和`C@1.0.0/node_modules` 文件夾 (3)處理直接依賴:A,C將被符號鏈接至根目錄的 `node_modules` 文件夾 ## 參考資料: [pnpm官網](https://pnpm.io/) [sample pnpm project](https://github.com/pnpm/sample-project) [https://blog.logrocket.com/javascript-package-managers-compared/](https://blog.logrocket.com/javascript-package-managers-compared/) [https://medium.com/@mitalisg/what-is-the-difference-between-a-hard-link-and-a-symbolic-link-e4043996047a](https://medium.com/@mitalisg/what-is-the-difference-between-a-hard-link-and-a-symbolic-link-e4043996047a) [https://zhuanlan.zhihu.com/p/352437367](https://zhuanlan.zhihu.com/p/352437367) [https://zhuanlan.zhihu.com/p/526257537](https://zhuanlan.zhihu.com/p/526257537)