前言
Yarn 團隊在春節前公佈了 Yarn 2.0 的規劃。其中提到了一個之前沒聽說過的名詞 “PnP”。發現 Yarn 的這個功能早在 18 年 9 月份就被提出並實現了。於是花了一些時間瞭解了一下它的工作原理以及解決的問題並整理除了本篇文章。
現狀與痛點
Yarn 團隊開發 PnP 特性最直接的原因就是現有的依賴管理方式效率太低。引用依賴時慢,安裝依賴時也慢。
先說說 Node 在處理依賴引用時的邏輯,這個流程會有如下兩種情況:
- 如果我們傳給
require()
呼叫的引數是一個核心模組(例如 "fs"、"path"等)或者是一個本地相對路徑(例如./module-a.js
或/my-li/module-b.js
),那麼 Node 會直接使用對應的檔案。 - 如果不是前面描述的情況,那麼 Node 會開始尋找一個名為
node_modules
的目錄:- 首先 Node 會在當前目錄尋找
node_modules
,如果沒有則到父目錄查詢,以此類推直到系統根目錄。 - 找到
node_modules
目錄之後,再在該目錄中尋找名為moduleName.js
的檔案或是名為moduleName
的子目錄。
- 首先 Node 會在當前目錄尋找
此處旨在說明問題,對 Node 內部模組解析邏輯做了簡化描述
可見 Node 在解析依賴時需要進行大量的檔案 I/O 操作,效率並不高。
再來看看安裝依賴時發生了什麼,現階段 yarn install
操作會執行以下 4 個步驟:
- 將依賴包的版本區間解析為某個具體的版本號
- 下載對應版本依賴的 tar 包到本地離線映象
- 將依賴從離線映象解壓到本地快取
- 將依賴從快取拷貝到當前目錄的
node_modules
目錄
其中第 4 步同樣涉及大量的檔案 I/O,導致安裝依賴時效率不高(尤其是在 CI 環境,每次都需要安裝全部依賴)。
Facebook 的工程師受夠了這些問題決定尋找一個能徹底解決問題同時還可以與現有生態相容的解決方案。這便是 Plug'n'Play 特性,簡稱 PnP。它已在 Facebook 內部測試了一段時間,現在 Yarn 團隊決定與社群分享並共同優化該方案。
實現方案
PnP 的具體工作原理是,作為把依賴從快取拷貝到 node_modules
的替代方案,Yarn 會維護一張靜態對映表,該表中包含了以下資訊:
- 當前依賴樹中包含了哪些依賴包的哪些版本
- 這些依賴包是如何互相關聯的
- 這些依賴包在檔案系統中的具體位置
這個對映表在 Yarn 的 PnP 實現中對應專案目錄中的 .pnp.js
檔案。
這個 .pnp.js
檔案是如何生成,Yarn 又是如何利用它的呢?
在安裝依賴時,在第 3 步完成之後,Yarn 並不會拷貝依賴到 node_modules
目錄,而是會在 .pnp.js
中記錄下該依賴在快取中的具體位置。這樣就避免了大量的 I/O 操作同時專案目錄也不會有 node_modules
目錄生成。
同時 .pnp.js
還包含了一個特殊的 resolver,Yarn 會利用這個特殊的 resolver 來處理 require()
請求,該 resolver 會根據 .pnp.js
檔案中包含的靜態對映表直接確定依賴在檔案系統中的具體位置,從而避免了現有實現在處理依賴引用時的 I/O 操作。
帶來了哪些好處
從 PnP 的實現方案可以看出,同一個系統上不同專案引用的相同依賴的相同版本實際都是指向的快取中的同一個目錄。這帶來了幾個最直觀的好處:
- 安裝依賴的速度得到了空前的提升
- CI 環境中多個 CI 例項可以共享同一份快取
- 同一個系統中的多個專案不再需要佔用多份磁碟空間
如何開始使用 Plug'n'Play 特性?
首先你需要 Yarn 1.12+ 版本。然後根據你的具體場景可以選擇:
使用 create-react-app
建立專案時開啟 PnP
create-react-app
已經整合了對 PnP 的支援。只需在建立專案時新增 --use-pnp
引數即可。
npx create-react-app testapp --use-pnp
複製程式碼
在已有專案中開啟 PnP
只需在專案中執行:
yarn --pnp
複製程式碼
即可開啟 PnP 特性。
注意事項
pkg.installConfig
欄位
在專案中開啟 PnP 特性後,Yarn 會在 package.json
檔案中建立一個 installConfig
欄位:
{
"installConfig": {
"pnp": true
}
}
複製程式碼
只要 installConfig.pnp
的值是一個真值且當前版本的 Yarn 支援,PnP 特性就會被啟用。
執行 npm script
或是執行 .js
檔案
由於在開啟了 PnP 的專案中不再有 node_modules
目錄,所有的依賴引用都必須由 .pnp.js
中的 resolver 處理。因此不論是執行 script 還是用 node
直接執行一個 JS 檔案,都必須經由 Yarn 處理。必須通過 yarn run
或是 yarn node
執行。
在專案中除錯依賴
在開發過程中我們有時會直接修改 node_modules
目錄下的依賴來除錯。但在 PnP 模式下,由於依賴都指向了全域性快取,我們不再可以直接修改這些依賴。
針對這種場景,Yarn 提供了 yarn unplug packageName
來將某個指定依賴拷貝到專案中的 .pnp/unplugged
目錄下。之後 .pnp.js
中的 resolver 就會自動載入這個 unplug 的版本。
除錯完畢後,再執行 yarn unplug --clear packageName
可移除本地 .pnp/unplugged
中的對應依賴。
總結
目前 PnP 還是一個相對比較新的特性,大家可以嘗試在本地開發環境中啟用 PnP 來感受一下它帶來的全新體驗。遇到問題可以及時反饋到 Yarn 的 issue 列表中。
參考連結:
- Yarn's Future - v2 and beyond
- Yarn Plug'n'Play: Getting rid of node_modules
- Plug’n’Play Whitepaper
- Yarn Plug'n'Play: Implementation
- yarnpkg.com/en/docs/pnp
更多文章,請關注我們團隊的公眾號:全棧探索