關(guān)于包管理器Npm、Yarn和Pnpm的一些總結(jié)
寫在前面
在Node.js生態(tài)系統(tǒng)中,包管理器是至關(guān)重要的組件之一,它們負(fù)責(zé)維護(hù)各種應(yīng)用程序和庫之間的依賴關(guān)系。npm是Node.js的默認(rèn)包管理器,它的初始版本是npm1,但是它很快就被npm2所取代。
npm2
關(guān)于npm2最初作為包管理管理,采用的是node_modules嵌套模式,即每個(gè)包都會(huì)有自己獨(dú)立的node_modules,且會(huì)將各自依賴進(jìn)行安裝,依賴的依賴也會(huì)產(chǎn)生自己的node_modules,這樣就產(chǎn)生了“嵌套依賴”。
就像回調(diào)嵌套一樣,容易陷入回調(diào)地獄,嵌套依賴也不例外。
這種嵌套依賴的模式,雖然可以使依賴項(xiàng)的版本更加明確和穩(wěn)定,但是在實(shí)際應(yīng)用中也存在一些問題。其中最大的問題是包的嵌套層級(jí)很深,這可能會(huì)導(dǎo)致安裝和更新依賴項(xiàng)的時(shí)間變長,并增加包的大小。此外,由于每個(gè)包都有自己的node_modules文件夾,這可能會(huì)導(dǎo)致文件系統(tǒng)中出現(xiàn)大量重復(fù)的依賴項(xiàng),從而占用更多的磁盤空間。
在實(shí)際操作中,當(dāng)需要在特定的Node.js版本中使用npm2時(shí),可以使用Node Version Manager (nvm)來管理多個(gè)Node.js版本。例如,在切換到Node.js v4.0版本時(shí),對(duì)應(yīng)的npm版本是npm2.x。
為了更好地說明嵌套依賴的問題,我們可以通過安裝koa來演示。koa是一個(gè)基于Node.js的Web應(yīng)用程序框架,它有許多依賴項(xiàng),我們可以使用以下命令來安裝koa:
在安裝koa時(shí),npm會(huì)自動(dòng)下載和安裝所有必需的依賴項(xiàng),并將它們安裝到koa的node_modules文件夾中。如果我們檢查koa的node_modules文件夾,我們會(huì)發(fā)現(xiàn)它包含了大量的依賴項(xiàng),這些依賴項(xiàng)中又包含了更多的依賴項(xiàng),導(dǎo)致整個(gè)文件夾的嵌套層級(jí)變得很深。
對(duì)于多包之間會(huì)存在公共依賴,如果對(duì)于每個(gè)依賴都生成自己獨(dú)立的node_modules,那么就會(huì)對(duì)相同包重復(fù)安裝多次,這就會(huì)占據(jù)很大的磁盤空間。且無限嵌套,也會(huì)超過windows的最大文件路徑長度限制(265個(gè)字符)。
嵌套依賴項(xiàng)的模式是npm2中的一個(gè)特性,雖然可以保證依賴項(xiàng)的版本穩(wěn)定性和精確性,但是它可能會(huì)導(dǎo)致嵌套層級(jí)變得很深,并占用大量的磁盤空間。
yarn
我們想到,既然樹形結(jié)構(gòu)存在弊端,為什么不將依賴包在根node_modules進(jìn)行扁平化處理,這不就解決了依賴嵌套、依賴重復(fù)和路徑限制問題了?
此時(shí)新方式y(tǒng)arn就橫空誕生。
當(dāng)使用yarn進(jìn)行依賴管理時(shí),我們可以看到所有依賴都會(huì)被安裝在根目錄下的node_modules文件夾中。與npm2不同的是,yarn采用了扁平依賴項(xiàng)的模式,這意味著相同的依賴包只會(huì)被安裝一次,并且不會(huì)存在多個(gè)嵌套的node_modules文件夾。
使用yarn add koa進(jìn)行安裝,可以看到通過yarn進(jìn)行管理的依賴全部平鋪在根node_modules下,且沒有重復(fù)依賴安裝的問題。
但是,當(dāng)某些依賴包存在多個(gè)版本時(shí),yarn會(huì)將其中一個(gè)版本提升到根node_modules文件夾中,而其他依賴包則會(huì)繼續(xù)維護(hù)自己的版本。這可能會(huì)導(dǎo)致某些依賴包無法正常工作,因?yàn)樗鼈兛赡苄枰褂锰囟ò姹镜囊蕾嚢?。為了解決這個(gè)問題,yarn仍然需要使用嵌套的node_modules文件夾,以確保每個(gè)依賴包使用正確的版本。
值得注意的是,yarn采用的扁平依賴項(xiàng)模式具有許多優(yōu)點(diǎn),例如更快的安裝速度,更少的磁盤空間占用和更少的依賴沖突問題。此外,yarn還提供了一個(gè)lock文件,該文件記錄了所有依賴項(xiàng)的確切版本和位置,以確保依賴項(xiàng)的版本穩(wěn)定性和一致性。
yarn的變與不變:
yarn采用了更加高效和可靠的依賴項(xiàng)管理方式,可以有效地避免依賴沖突和嵌套的問題。但是,對(duì)于某些多版本依賴包,yarn仍然需要使用node_modules嵌套的方式來確保每個(gè)依賴包都使用正確的版本。
npm3
npm3在2015年發(fā)布時(shí)引入了一種新的依賴項(xiàng)安裝算法,稱為“扁平依賴項(xiàng)”。其主要原理是通過將所有依賴項(xiàng)都放置在同一個(gè)目錄下,并使用符號(hào)鏈接來實(shí)現(xiàn)依賴項(xiàng)的共享。
在npm3中,所有依賴項(xiàng)都被直接安裝到根目錄下的node_modules中,而不是像npm2一樣在每個(gè)依賴包中嵌套一個(gè)node_modules目錄。這種扁平化的結(jié)構(gòu)可以減少依賴項(xiàng)的嵌套層級(jí),從而降低了磁盤空間的占用和文件路徑的長度。在這種模式下,所有依賴項(xiàng)都被安裝到頂級(jí)node_modules文件夾中,這樣就避免了嵌套依賴項(xiàng)的問題。這種模式雖然簡單,但是它可能會(huì)導(dǎo)致依賴項(xiàng)的版本不穩(wěn)定,從而可能會(huì)導(dǎo)致依賴沖突的問題。
當(dāng)我們使用npm3安裝koa包時(shí),它會(huì)首先檢查該包所需的所有依賴項(xiàng)是否已經(jīng)安裝,如果沒有安裝,則會(huì)將這些依賴項(xiàng)直接安裝到根目錄下的node_modules目錄中。同時(shí),npm3會(huì)使用符號(hào)鏈接將這些依賴項(xiàng)鏈接到需要使用它們的包的node_modules目錄下。
通過使用符號(hào)鏈接,npm3可以實(shí)現(xiàn)依賴項(xiàng)的共享,從而避免了依賴項(xiàng)的重復(fù)安裝和占用大量的磁盤空間。此外,npm3還支持npm shrinkwrap命令,可以生成一個(gè)lockfile文件,記錄每個(gè)包所使用的依賴項(xiàng)的精確版本號(hào),從而避免了版本沖突和不兼容的問題。
shrinkwrap 文件的作用是什么?
這個(gè)文件用于記錄整個(gè)依賴樹的結(jié)構(gòu)和依賴包的版本信息,可以保證依賴包的版本穩(wěn)定性和一致性。
那么使用扁平化方案就能完美解決以上問題嗎?當(dāng)然不是。
- 幽靈依賴:在聲明中沒有使用dependencies中的依賴,代碼中也可以進(jìn)行reqiure引入。沒有在項(xiàng)目中進(jìn)行顯式依賴,如果別的包不再依賴這個(gè)包,就會(huì)導(dǎo)致代碼因?yàn)橐蕾囘@個(gè)包,而沒有進(jìn)行安裝,最終不能正常運(yùn)行。
- 磁盤浪費(fèi):對(duì)于依賴包只會(huì)提升一個(gè),存在多個(gè)版本時(shí)其余包同樣得進(jìn)行拷貝到各自node_modules下,依然會(huì)存在磁盤空間浪費(fèi)。
什么是幽靈依賴?
在安裝和使用某個(gè)第三方包時(shí),該包依賴的其他依賴沒有在它的js文件中顯式引入的情況。這些依賴可能在代碼中被引用,但是沒有被包含在軟件包的package.json文件中。這種情況被稱為“幽靈依賴”。
舉個(gè)例子,假設(shè)有個(gè)項(xiàng)目需要依賴包 A 和 B,而這兩個(gè)包都依賴于包 C,但是包 A 依賴于包 C 的版本 1.0.0,而包 B 依賴于包 C 的版本 2.0.0。在 npm2 中,這兩個(gè)版本的包 C 會(huì)被分別安裝在 A 和 B 的 node_modules 目錄下,不會(huì)產(chǎn)生沖突。但在 npm3 中,這兩個(gè)版本的包 C 可能會(huì)被安裝在同一個(gè) node_modules 目錄下,這時(shí)候就會(huì)產(chǎn)生沖突,導(dǎo)致代碼無法運(yùn)行。
雖然在npm3提供了 npm dedupe 命令,可以是手動(dòng)輸入命令將重復(fù)的依賴項(xiàng)合并到頂層 node_modules 目錄下,避免了幽靈依賴的問題。但是好像并沒有很智能。
總的來說,npm3通過采用扁平化的依賴管理結(jié)構(gòu)和符號(hào)鏈接機(jī)制,引入 shrinkwrap 文件實(shí)現(xiàn)了依賴項(xiàng)的共享和版本精確控制,并且減少了依賴項(xiàng)的嵌套層級(jí)和磁盤空間占用。可以手動(dòng)使用 dedupe 命令等方式,解決了 npm2 中出現(xiàn)的幽靈依賴問題,提高了包管理的效率和可靠性。
pnpm
針對(duì)上面遺留下的兩個(gè)問題,pnpm橫空出世,采用硬鏈接和符號(hào)鏈接來管理依賴項(xiàng),以減少重復(fù)下載和占用空間,從而有效地解決幽靈依賴和磁盤浪費(fèi)的問題。
- 基于內(nèi)容尋址的文件系統(tǒng)來存儲(chǔ)磁盤的文件
- 不會(huì)重復(fù)安裝同一個(gè)包,只在磁盤中寫入一次,而后在使用的地方通過hardlink
- 同包不同版本,也盡可能復(fù)用代碼
link:也就是軟硬連接,這是操作系統(tǒng)提供的機(jī)制。
- 硬連接就是同一個(gè)文件的不同引用
- 軟鏈接是新建一個(gè)文件,文件內(nèi)容指向另一個(gè)路徑
具體來說,當(dāng)使用 pnpm 安裝koa包的依賴項(xiàng)時(shí),它會(huì)首先檢查系統(tǒng)上是否已經(jīng)安裝了所需的依賴項(xiàng)。如果已經(jīng)安裝,則 pnpm 將創(chuàng)建一個(gè)符號(hào)鏈接到該依賴項(xiàng),而不是在當(dāng)前項(xiàng)目中復(fù)制該依賴項(xiàng)。這樣就避免了重復(fù)下載和占用磁盤空間的問題。
我們?cè)诿钚休斎?
此外,pnpm 還支持不同的包引用方式,如路徑引用和 git 倉庫引用,這使得 pnpm 可以更快地安裝依賴項(xiàng)并減少重復(fù)下載,從而提高開發(fā)效率和依賴項(xiàng)管理的可靠性。通過將包從全局 store 進(jìn)行硬鏈接到項(xiàng)目的虛擬 store 中,pnpm 可以避免多次拷貝文件和深度嵌套路徑過長的問題,從而進(jìn)一步減少磁盤空間的占用和提高性能。
PNPM 的核心思想是在整個(gè)項(xiàng)目內(nèi)共享依賴項(xiàng),而不是每個(gè)項(xiàng)目都擁有自己的依賴項(xiàng)副本。
這是官方文檔提供的原理圖:
可以看到有個(gè)公共的依賴包安裝池,然后通過軟鏈接引入到各個(gè)項(xiàng)目所需要的依賴中,這樣就減少了幽靈依賴、依賴嵌套和重復(fù)下載的問題。
PNPM的優(yōu)點(diǎn)如下:
節(jié)省磁盤空間:pnpm采用鏈接的方式將依賴項(xiàng)共享到全局store中,避免了每個(gè)項(xiàng)目都需要拷貝一份依賴包的問題,從而顯著減少了磁盤占用空間。
提升安裝速度:pnpm不需要每次都下載相同的依賴項(xiàng),而是從全局store中直接鏈接到各個(gè)項(xiàng)目中,因此可以極大地提高安裝速度。
避免了幽靈依賴、重復(fù)依賴和依賴嵌套:pnpm采用鏈接的方式,避免了項(xiàng)目之間依賴相同包不一致的問題,同時(shí)避免了重復(fù)安裝相同版本的依賴項(xiàng)和依賴嵌套的問題。
支持多種包引用方式:pnpm支持路徑引用和git倉庫引用,可以更加靈活地管理依賴項(xiàng)。
天生支持monorepo管理:得益于pnpm的軟鏈接特性,可以在同一個(gè)workspace下共享依賴和模塊等。
另外,對(duì)于存儲(chǔ)大量依賴的情況,pnpm提供了「pnpm store prune」命令,可以定期清理不再使用的依賴項(xiàng),釋放磁盤空間。
參考文章
- 關(guān)于現(xiàn)代包管理器的深度思考——為什么現(xiàn)在我更推薦 pnpm 而不是 npm/yarn?
- pnpm 是憑什么對(duì) npm 和 yarn 降維打擊的
- 現(xiàn)代前端工程為什么越來越離不開 Monorepo?
- 現(xiàn)代前端工程為什么越來越離不開 Monorepo?
- 為什么越來越多的項(xiàng)目選擇 Monorepo?
- pnpm官方文檔
寫在最后
最后對(duì)不同包管理器的優(yōu)缺點(diǎn)、特點(diǎn)做了一些總結(jié):
學(xué)而知不足,水平有限,還望諸君多多指教。覺得文章不錯(cuò)的讀者,不妨點(diǎn)個(gè)關(guān)注,收藏起來上班摸魚的時(shí)候品嘗。