先驅者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)方法,為修復node_modules的策略
node_modules
,生成一個帶有依賴查找表的.pnp.cjs
文件(非嵌套結構).yarn/cache/
(省磁盤空間)⇒ 要求維護者更新現有的package
⇒ 許多知名開發人員公開批評Yarn 2
⇒ 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
polyrepo ⇒ dependencies 的版本管理複雜/重覆配置/共用程式碼維護成本高
4. 安全性高
https://github.com/pnpm/benchmarks-of-javascript-package-managers
在npm1、npm2中=>嵌套結構
在npm3 、yarn (V1)中=>扁平化依賴(將子依賴提升hoist)
新問題產生
不確定性是指:同樣的package.json,安裝後可能得到不同node_modules 的目錄結構。
Q:A 依賴B@1.0,C 依賴B@2.0,安裝後究竟應該提升B 的1.0 還是C的2.0?
A:都有可能,取決於用戶的安裝順序。取決於A 和C 在 package.json
中的位置,如果A 聲明在前面,那麼就是前面的結構。
無論是
package-lock.json
(npm 5.x才出現)還是yarn.lock
,都是為了保證install 之後都產生確定的node_modules
結構。lockfile 裡記錄了依賴,以及依賴的子依賴,依賴的版本,獲取地址與驗證模塊完整性的hash。
即使是不同的安裝順序,相同的依賴關係在任何的環境和容器中,都能得到穩定的node_modules 目錄結構,保證了依賴安裝的確定性。
幽靈依賴是指:在package.json 中未定義的依賴,但項目中依然可以正確地被引用到。
比如上方的示例其實我們只安裝了A和C
由於B 在安裝時被提升到了和A 同樣的層級,所以在專案中引用B 能正常運作。
如果某天某個版本的A 依賴不再依賴B 或者B 的版本有所變化,那麼就會造成依賴缺失問題。
ex: 再安裝依賴@B2.0 的D ⇒這兩個重複安裝的B 就叫doppelgangers
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` 文件夾
https://blog.logrocket.com/javascript-package-managers-compared/