# pnpm - 套件管理工具小背景 - **先驅者npm** 2010 年1 月發布 2020 年,GitHub 收購了npm,原則上 npm 現在歸微軟管理 - 關於npm冷知識 Q:npm 縮寫是 node package manager? A:npm 不是任何短語的縮寫 > npm 官方闢謠: >  - **創新者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://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**  將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`。  (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)
×
Sign in
Email
Password
Forgot password
or
Sign in via Google
Sign in via Facebook
Sign in via X(Twitter)
Sign in via GitHub
Sign in via Dropbox
Sign in with Wallet
Wallet (
)
Connect another wallet
Continue with a different method
New to HackMD?
Sign up
By signing in, you agree to our
terms of service
.