前言
在現代化的開發中,一個人可能同時開發多個項目,安裝的項目越來越多,所隨之安裝的依賴包也越來越臃腫,而且有時候所安裝的速度也很慢,甚至會安裝失敗。
因此我們就需要去了解一下,我們的包管理器,在前端比較主流的包管理器主要有三個(當然還有其他優秀的包管理器,本文主要介紹這三個),分別是:npm,yarn,pnpm
幽靈嵌套(Phantom Dependency)
在了解包管理器之前,我們先了解一下包管理的一個難題:幽靈嵌套
幽靈嵌套問題通常發生在依賴之間存在復雜的版本要求時,比如:
- 包 A 依賴于包 B@1.0.0
- 包 B 依賴于包 C@2.0.0
- 另一個包 D 也依賴于 C@3.0.0
在傳統的依賴管理中,可能會導致包 C 的不同版本被嵌套在不同的子依賴樹中,從而在 node_modules
中形成不同路徑的多層嵌套,導致路徑非常深。這種情況一旦發生,如果包 A 和包 D 不同意版本要求,可能會導致不同版本的包 C 被分別安裝在不同的路徑下,出現路徑沖突甚至依賴問題,這就是“幽靈嵌套”現象。
NPM (Node Package Manager)
概述:
npm 是 Node.js 默認的包管理工具,最早由 Node 社區開發并捆綁到 Node.js 中,因此使用最為廣泛。
從npm v2 到npm v7+的升級過程中,從最初使用遞歸的方法處理依賴,造成高度嵌套的依賴樹,到后來使用扁平化管理,一定程度上解決了依賴嵌套問題,但是出現了算法時間過長的問題,最后引入了package-lock.json機制,作用是鎖定依賴結構,一定程度上保持了依賴的穩定性
核心:
- 采用扁平化依賴管理管理方式
- 每個依賴包都會在 node_modules 中單獨安裝
- 相同的依賴可能會被重復安裝多次
特點:
- 優點:
- Node.js 默認包管理器,使用最廣泛,擁有強大的額社區支持
- 最早的包管理器,簡單易上手,對初學者友好
- package-lock.json 保證依賴版本一致性
- 缺點:
- 安裝速度較慢,占用空間大
目錄結構:
- npm v2及以前
- 依賴樹可能非常深
- 相同包會重復安裝
- 占用大量磁盤空間
- 文件路徑可能超過 Windows 限制
node_modules
├── A
│ └── node_modules
│ └── B
│ └── node_modules
│ └── C
└── D└── node_modules└── B└── node_modules└── C
- npm v3-6,扁平化管理
- 采用扁平化優先的安裝策略
- 相同版本的包會被提升到頂層
- 不同版本保留在各自的 node_modules 中
- 安裝算法比較復雜,需要計算依賴樹
// 假設依賴關系:
// package-A 依賴 lodash@4.0.0
// package-B 依賴 lodash@4.0.0node_modules
├── package-A
├── package-B
└── lodash // 被提升到頂層
當有版本沖突時:
// package-A 依賴 lodash@4.0.0
// package-B 依賴 lodash@3.0.0node_modules
├── package-A
├── package-B
│ └── node_modules
│ └── lodash // 3.0.0 版本
└── lodash // 4.0.0 版本提升到頂層
- npm v7+, 改進了扁平化管理,引入peer dependencies 處理
- 自動安裝 peer dependencies
- 更嚴格的版本鎖定
- 改進了依賴解析算法
- workspaces 支持
node_modules
├── package-A
├── package-B
├── lodash // 主版本
└── .package-lock.json // 更嚴格的版本鎖定
Yarn
概述:
Yarn 是一個 JavaScript 包管理工具,最早由 Facebook 推出,主要用于管理項目中的依賴包。和 npm 類似,yarn 解決了在 JavaScript 項目中下載、安裝和管理依賴的需求,并在一定程度上改進了 npm 的一些缺點,比如性能、穩定性和安全性。
核心:
- 并行下載提升安裝速度:
傳統的 npm 安裝方式是依次下載依賴,而 Yarn 可以同時下載多個依賴,稱為“并行下載”。這種方式充分利用了網絡帶寬,顯著減少安裝依賴所需的時間,使得安裝速度更快。并行下載尤其在大型項目中效果顯著,能夠有效降低整體安裝時間。
- 緩存機制減少重復下載:
Yarn 內置了緩存機制,在首次安裝依賴時會將其緩存到本地。之后再次安裝這些依賴時,如果依賴版本沒有改變,Yarn 會直接從本地緩存中讀取,而不是重新下載。這樣不僅節省了網絡請求,還提升了安裝速度,特別適合離線開發和持續集成場景。
- yarn.lock 確保依賴版本一致性:
Yarn 使用 yarn.lock 文件記錄每個依賴的具體版本和來源,確保團隊所有成員在不同機器上安裝時得到的依賴版本完全一致。這避免了“依賴地獄”問題,即由于版本不一致導致的錯誤或不兼容情況,從而提高了開發過程的穩定性。
- 更安全的依賴解析機制:
Yarn 在安裝依賴時會校驗每個包的完整性(如 SHA 校驗),以確保包的內容沒有被篡改。這種安全機制能在下載依賴時檢測到潛在的包篡改或惡意代碼的引入,增強了項目的安全性。相比于早期的 npm,Yarn 的這種依賴解析機制更加嚴謹。
- Workspace 支持更好的 monorepo 管理:
Yarn 支持 Workspace 功能,允許在單個代碼庫(monorepo)中管理多個項目或包。這種管理方式可以將多個子項目的依賴集中管理、共享,減少重復依賴的安裝。此外,Yarn 還能通過 Workspace 在多個包之間建立相互依賴關系,使 monorepo 項目的開發、構建和測試更加高效。
- PnP(Plug’n’Play)模式提供更快的模塊加載:
Yarn 2.x 引入了 PnP 模式,這種模式完全去除了 node_modules 目錄,通過在 .pnp.cjs 文件中記錄依賴映射關系。PnP 不僅減少了磁盤空間的占用,還提升了依賴的加載速度,因為 Node.js 不再需要遞歸遍歷 node_modules。這樣可以加快應用的啟動速度,同時在依賴數量龐大的項目中減少文件系統的壓力。
特點:
- 優點:
- 采用扁平化優先 + 符號鏈接(符號鏈接是一個特殊的文件,它包含對另一個文件或目錄的引用路徑)的組合策略
- 相同版本的包會被提升并復用
- 不同版本通過符號鏈接保持正確的引用關系
- 缺點:
-
仍然存在幽靈依賴問題:
盡管 Yarn 已經在扁平化和依賴管理上做了優化,但在一些復雜的項目中仍然會出現幽靈依賴問題。所謂幽靈依賴,指的是某個包在項目中使用但并未在 package.json 中聲明,可能是通過其他依賴的間接依賴引入。這種隱式依賴會導致項目依賴關系難以維護,如果間接依賴被移除,可能會導致項目出錯。 -
某些場景下的依賴解析較慢:
Yarn 的依賴解析雖然比傳統 npm 更快,但在依賴結構復雜、依賴版本沖突較多的情況下,解析和處理依賴關系可能會變慢。尤其在 monorepo 中,Yarn 需要處理多個包之間的依賴關系,可能出現解析速度不如 pnpm 的情況。
目錄結構:
- 基本結構
node_modules/
├── package-A/ # 實際文件
├── package-B/ # 實際文件
├── lodash/ # 提升到頂層的共享包
└── .bin/ # 可執行文件的符號鏈接
- 依賴共享案例
// 假設有以下依賴關系:
項目
├── package-A (依賴 lodash@4.0.0)
└── package-B (依賴 lodash@4.0.0)// Yarn 會創建這樣的結構:
node_modules/
├── package-A/
│ └── node_modules/
│ └── lodash -> ../../../lodash # 符號鏈接
├── package-B/
│ └── node_modules/
│ └── lodash -> ../../../lodash # 符號鏈接
└── lodash/ # 實際文件
- 版本沖突處理
// 當存在版本沖突時:
項目
├── package-A (依賴 lodash@4.0.0)
└── package-B (依賴 lodash@3.0.0)// Yarn 會這樣處理:
node_modules/
├── package-A/
│ └── node_modules/
│ └── lodash -> ../../../lodash # 指向4.0.0
├── package-B/
│ └── node_modules/
│ └── lodash/ # 本地安裝3.0.0
└── lodash/ # 4.0.0版本在頂層
PNPM(Performant NPM)
概述:
pnpm 是一個更現代化的包管理工具,旨在解決 npm 和 yarn 的一些效率和資源管理問題。
- 核心:
- 采用內容尋址存儲系統:
pnpm 使用內容尋址(content-addressable storage)來存儲依賴包。每個依賴包都會被哈希處理,并根據其內容生成唯一的存儲地址。這樣,即使多個項目依賴于相同版本的包,pnpm 也只需要存儲一份,不會重復存儲同樣內容的文件。
- 使用硬鏈接和符號鏈接共享依賴:
pnpm 通過在 node_modules 中創建硬鏈接或符號鏈接(symlink),指向內容尋址存儲中實際的依賴包。這樣每個項目可以“共享”依賴,而不必為每個項目單獨存儲依賴包內容。
- 硬鏈接(Hard Link) :將文件內容鏈接到項目文件夾下,不占用額外磁盤空間。
- 符號鏈接(Symlink) :為特定版本的包創建路徑映射,使項目代碼能夠準確找到每個依賴包版本的地址。
特點:
- 優點:
- 顯著節省磁盤空間
- 安裝速度快
- 更嚴格的依賴管理
- pnpm-lock.yaml 確保依賴版本一致
- 缺點:
-
不兼容一些使用傳統 node_modules 結構的工具和插件:
-
在 pnpm 中,每個依賴都有自己的隔離路徑,某些工具、插件或構建系統可能會假設 node_modules 目錄是扁平的,這可能導致兼容性問題。
-
與本地開發和測試環境的潛在不兼容:
-
有些項目依賴于本地 node_modules 結構,或者需要直接訪問 node_modules 中的文件。在 pnpm 使用內容尋址和符號鏈接時,這可能會導致某些工具無法正常運行。
目錄結構
- 內容尋址存儲
.pnpm-store/
└── v3/└── files/├── 00/ # 前兩位哈希值作為目錄名│ └── deadbeef... # 包內容的哈希值└── ff/└── cafebabe... # 另一個包的哈希值
- 依賴結構
node_modules/
├── .pnpm/
│ ├── react@17.0.2/
│ │ └── node_modules/
│ │ ├── react/ # 實際文件(硬鏈接到 store)
│ │ └── loose-envify/ # react 的依賴
│ └── lodash@4.17.21/
│ └── node_modules/
│ └── lodash/ # 實際文件(硬鏈接到 store)
├── react -> .pnpm/react@17.0.2/node_modules/react # 符號鏈接
└── lodash -> .pnpm/lodash@4.17.21/node_modules/lodash # 符號鏈接
目錄說明
-
.pnpm/ 文件夾:存放項目的所有依賴包,按 包名@版本號 命名,并在其 node_modules 文件夾中包含該包的實際文件和依賴項。
-
硬鏈接:.pnpm 中的實際文件并不是直接復制到每個項目中,而是通過硬鏈接指向 pnpm 的全局緩存存儲目錄 (pnpm store)。這樣,不同項目間的相同版本依賴不需要重復下載。
-
符號鏈接:pnpm 會在項目的 node_modules 根目錄創建符號鏈接,將每個包鏈接到 .pnpm 中實際的包路徑。例如:
-
node_modules/react 是一個符號鏈接,指向 .pnpm/react@17.0.2/node_modules/react
node_modules/lodash 符號鏈接指向 .pnpm/lodash@4.17.21/node_modules/lodash
工作原理
- 包安裝:pnpm 會將依賴包下載到全局緩存 (pnpm store) 中,并將實際文件硬鏈接到 .pnpm 文件夾中的特定版本目錄下。
- 創建符號鏈接:在項目的 node_modules 文件夾內創建符號鏈接,將包名稱指向 .pnpm 中的對應路徑。
- 引用:項目中的 require(‘react’) 會自動找到 node_modules/react 符號鏈接,并通過符號鏈接訪問實際文件。
總結:
三者同異:
使用選擇:
基于這些特點:
- 如果項目體積較小,團隊成員 Node.js 經驗不同,推薦使用 npm
- 如果需要更好的性能和可靠性,推薦使用 yarn
- 如果需要最嚴格的依賴管理、最小的磁盤空間占用,推薦使用 pnpm
常用命令:
npm、yarn 和 pnpm 的常用命令對比表: