pnpm才是前端工程化項(xiàng)目的未來
前言?
相信小伙伴們都接觸過npm/yarn?,這兩種包管理工具想必是大家工作中用的最多的包管理工具,npm?作為node?官方的包管理工具,它是隨著node的誕生一起出現(xiàn)在大家的視野中,而yarn?的出現(xiàn)則是為了解決npm?帶來的諸多問題,雖然yarn?提高了依賴包的安裝速度與使用體驗(yàn),但它依舊沒有解決npm?的依賴重復(fù)安裝等致命問題?!竝npm」的出現(xiàn)完美解決了依賴包重復(fù)安裝的問題,并且實(shí)現(xiàn)了yarn帶來的所有優(yōu)秀體驗(yàn),所以說「pnpm才是前端工程化項(xiàng)目的未來」。
npm 與 yarn 存在的問題?
早期的npm
在npm@3之前,node_modules?結(jié)構(gòu)可以說是整潔?、可預(yù)測(cè)的,因?yàn)楫?dāng)時(shí)的依賴結(jié)構(gòu)是這樣的:
node_modules
└─ 依賴A
├─ index.js
├─ package.json
└─ node_modules
└─ 依賴B
├─ index.js
└─ package.json
└─ 依賴C
├─ index.js
├─ package.json
└─ node_modules
└─ 依賴B
├─ index.js
└─ package.json
每個(gè)依賴下面都維護(hù)著自己的node_modules,這樣看起來確實(shí)非常整潔,但同時(shí)也帶來一些較為嚴(yán)重的問題:
- 依賴包重復(fù)安裝
- 依賴層級(jí)過多
- 模塊實(shí)例無法共享
依賴包重復(fù)安裝
從上面的依賴結(jié)構(gòu)我們可以看出,依賴A與依賴C同時(shí)引用了依賴B,此時(shí)的依賴B會(huì)被下載兩次。此刻我們想想要是某一個(gè)依賴被引用了n次,那么它就需要被下載n次。(此時(shí)心里是不是在想,怎么會(huì)有如此坑的設(shè)計(jì))
依賴層級(jí)過多
我們?cè)賮砜戳硗庖环N依賴結(jié)構(gòu):
node_modules
└─ 依賴A
├─ index.js
├─ package.json
└─ node_modules
└─ 依賴B
├─ index.js
├─ package.json
└─ node_modules
└─ 依賴C
├─ index.js
├─ package.json
└─ node_modules
└─ 依賴D
├─ index.js
└─ package.json
這種依賴層級(jí)少還能接受,要是依賴層級(jí)多了,這樣一層一層嵌套下去,就像一個(gè)依賴地獄,不利于維護(hù)。
npm@3與yarn
為了解決上述問題,npm3?與yarn?都選擇了扁平化結(jié)構(gòu),也就是說現(xiàn)在我們看到的node_modules里面的結(jié)構(gòu)不再有依賴嵌套了,都是如下依賴結(jié)構(gòu):
node_modules
└─ 依賴A
├─ index.js
├─ package.json
└─ node_modules
└─ 依賴C
├─ index.js
├─ package.json
└─ node_modules
└─ 依賴B
├─ index.js
├─ package.json
└─ node_modules
node_modules下所有的依賴都會(huì)平鋪到同一層級(jí)。由于require尋找包的機(jī)制,如果A和C都依賴了B,那么A和C在自己的node_modules中未找到依賴C的時(shí)候會(huì)向上尋找,并最終在與他們同級(jí)的node_modules中找到依賴包C。這樣就不會(huì)出現(xiàn)重復(fù)下載的情況。而且依賴層級(jí)嵌套也不會(huì)太深。因?yàn)闆]有重復(fù)的下載,所有的A和C都會(huì)尋找并依賴于同一個(gè)B包。自然也就解決了實(shí)例無法共享數(shù)據(jù)的問題。
由于這個(gè)扁平化結(jié)構(gòu)的特點(diǎn),想必大家都遇到了這樣的體驗(yàn),自己明明就只安裝了一個(gè)依賴包,打開node_modules文件夾一看,里面卻有一大堆。
這種扁平化結(jié)構(gòu)雖然是解決了之前的嵌套問題,但同時(shí)也帶來了另外一些問題:
- 依賴結(jié)構(gòu)的不確定性
- 扁平化算法的復(fù)雜度增加
- 項(xiàng)目中仍然可以非法訪問沒有聲明過的依賴包(幽靈依賴)
依賴結(jié)構(gòu)的不確定性
這個(gè)怎么理解,為什么會(huì)產(chǎn)生這種問題呢?我們來仔細(xì)想想,加入有如下一種依賴結(jié)構(gòu):
A包與B包同時(shí)依賴了C包的不同版本,由于同一目錄下不能出現(xiàn)兩個(gè)同名文件,所以這種情況下同一層級(jí)只能存在一個(gè)版本的包,另外一個(gè)版本還是要被嵌套依賴。
那么問題又來了,既然是要一個(gè)扁平化一個(gè)嵌套,那么這時(shí)候是如何確定哪一個(gè)扁平化哪一個(gè)嵌套的呢?
這兩種結(jié)構(gòu)都有可能,準(zhǔn)確點(diǎn)說哪個(gè)版本的包被提升,取決于包的安裝順序!
這就是為什么會(huì)產(chǎn)生依賴結(jié)構(gòu)的不確定?問題,也是 lock 文件?誕生的原因,無論是package-lock.json?(npm 5.x 才出現(xiàn))還是yarn.lock?,都是為了保證 install 之后都產(chǎn)生確定的node_modules結(jié)構(gòu)。
盡管如此,npm/yarn 本身還是存在扁平化算法復(fù)雜?和package 非法訪問的問題,影響性能和安全。
pnpm?
前面說了那么多的npm?與yarn的缺點(diǎn),現(xiàn)在再來看看pnpm是如何解決這些尷尬問題的。
什么是pnpm
快速的,節(jié)省磁盤空間的包管理工具
就這么簡單,說白了它跟npm?與yarn沒有區(qū)別,都是包管理工具。但它的獨(dú)特之處在于:
- 包安裝速度極快
- 磁盤空間利用非常高效
特性
安裝包速度快
從上圖可以看出,pnpm的包安裝速度明顯快于其它包管理工具。那么它為什么會(huì)比其它包管理工具快呢?
我們來可以來看一下各自的安裝流程
- npm/yarn
- resolving:首先他們會(huì)解析依賴樹,決定要fetch哪些安裝包。
- fetching:安裝去fetch依賴的tar包。這個(gè)階段可以同時(shí)下載多個(gè),來增加速度。
- wrting:然后解壓包,根據(jù)文件構(gòu)建出真正的依賴樹,這個(gè)階段需要大量文件IO操作。
- pnpm
上圖是pnpm的安裝流程,可以看到針對(duì)每個(gè)包的三個(gè)流程都是平行的,所以速度會(huì)快很多。當(dāng)然pnpm會(huì)多一個(gè)階段,就是通過鏈接組織起真正的依賴樹目錄結(jié)構(gòu)。
磁盤空間利用非常高效
pnpm 內(nèi)部使用基于內(nèi)容尋址的文件系統(tǒng)來存儲(chǔ)磁盤上所有的文件,這個(gè)文件系統(tǒng)出色的地方在于:
- 不會(huì)重復(fù)安裝同一個(gè)包。用 npm/yarn 的時(shí)候,如果 100 個(gè)項(xiàng)目都依賴 lodash,那么 lodash 很可能就被安裝了 100 次,磁盤中就有 100 個(gè)地方寫入了這部分代碼。但在使用 pnpm 只會(huì)安裝一次,磁盤中只有一個(gè)地方寫入,后面再次使用都會(huì)直接使用hardlink。
- 即使一個(gè)包的不同版本,pnpm 也會(huì)極大程度地復(fù)用之前版本的代碼。舉個(gè)例子,比如 lodash 有 100 個(gè)文件,更新版本之后多了一個(gè)文件,那么磁盤當(dāng)中并不會(huì)重新寫入 101 個(gè)文件,而是保留原來的 100 個(gè)文件的hardlink,僅僅寫入那一個(gè)新增的文件。
支持monorepo
pnpm 與 npm/yarn 另外一個(gè)很大的不同就是支持了 monorepo,pnpm內(nèi)置了對(duì)monorepo的支持,只需在工作空間的根目錄創(chuàng)建pnpm-workspace.yaml和.npmrc配置文件,同時(shí)還支持多種配置,相比較lerna和yarn workspace,pnpm解決monorepo的同時(shí),也解決了傳統(tǒng)方案引入的問題。
monorepo 的宗旨就是用一個(gè) git 倉庫來管理多個(gè)子項(xiàng)目,所有的子項(xiàng)目都存放在根目錄的packages目錄下,那么一個(gè)子項(xiàng)目就代表一個(gè)package。
依賴管理
pnpm使用的是npm version 2.x類似的嵌套結(jié)構(gòu),同時(shí)使用.pnpm 以平鋪的形式儲(chǔ)存著所有的包。然后使用Store + Links和文件資源進(jìn)行關(guān)聯(lián)。簡單說pnpm把會(huì)包下載到一個(gè)公共目錄,如果某個(gè)依賴在 sotre 目錄中存在了話,那么就會(huì)直接從 store 目錄里面去 hard-link,避免了二次安裝帶來的時(shí)間消耗,如果依賴在 store 目錄里面不存在的話,就會(huì)去下載一次。通過Store + hard link的方式,使得項(xiàng)目中不存在NPM依賴地獄問題,從而完美解決了npm3+和yarn中的包重復(fù)問題。
我們分別用npm與pnpm來安裝vite對(duì)比看一下
npm | pnpm |
所有依賴包平鋪在node_modules目錄,包括直接依賴包以及其他次級(jí)依賴包 | node_modules?目錄下只有.pnpm和直接依賴包,沒有其他次級(jí)依賴包 |
沒有符號(hào)鏈接(軟鏈接) | 直接依賴包的后面有符號(hào)鏈接(軟鏈接)的標(biāo)識(shí) |
pnpm安裝的vite 所有的依賴都軟鏈至了 node_modules/.pnpm/ 中的對(duì)應(yīng)目錄。把 vite 的依賴放置在同一級(jí)別避免了循環(huán)的軟鏈。
軟鏈接和硬鏈接機(jī)制
pnpm 是通過 hardlink 在全局里面搞個(gè) store 目錄來存儲(chǔ) node_modules 依賴?yán)锩娴?hard link 地址,然后在引用依賴的時(shí)候則是通過 symlink 去找到對(duì)應(yīng)虛擬磁盤目錄下(.pnpm 目錄)的依賴地址。
這兩者結(jié)合在一起工作之后,假如有一個(gè)項(xiàng)目依賴了 A@1.0.0 和 B@1.0.0 ,那么最后的 node_modules 結(jié)構(gòu)呈現(xiàn)出來的依賴結(jié)構(gòu)可能會(huì)是這樣的:
node_modules
└── A
└── B
└── .pnpm
├── A@1.0.0
│ └── node_modules
│ └── A -> <store>/A
│ ├── index.js
│ └── package.json
└── B@1.0.0
└── node_modules
└── B -> <store>/B
├── index.js
└── package.json
node_modules 中的 A 和 B 兩個(gè)目錄會(huì)軟連接到 .pnpm 這個(gè)目錄下的真實(shí)依賴中,而這些真實(shí)依賴則是通過 hard link 存儲(chǔ)到全局的 store 目錄中。
store
?pnpm?下載的依賴全部都存儲(chǔ)到?store?中去了,?store?是?pnpm?在硬盤上的公共存儲(chǔ)空間。
pnpm?的store?在Mac/linux中默認(rèn)會(huì)設(shè)置到{home dir}>/.pnpm-store/v3;windows下會(huì)設(shè)置到當(dāng)前盤符的根目錄下。使用名為 .pnpm-store的文件夾名稱。
項(xiàng)目中所有.pnpm/依賴名@版本號(hào)/node_modules/?下的軟連接都會(huì)連接到pnpm的store中去。