使用 Yarn(v1.12+)的 Plug'n'Play 機制來取代 node_modules. 目前這仍是一個實驗性的特性.javascript
原文連接: Yarn Plug'n'Play能否助你脫離node_modules苦海?html
node_modules
早就成爲的全民吐槽的對象, 其餘語言的開發者看到 node_modules 對 Node 就望而祛步了, 用一個字來形容的話就是'重!'.前端
若是不瞭解 Node 模塊查找機制, 請點擊require() 源碼解讀java
一個簡單的前端項目(create-react-app)的大小和文件數:node
而 macOS 的/Library
目錄的大小的文件數:react
一行hello world
就須要安裝 130MB 以上的依賴模塊, 並且文件數是32,313. 相比之下 macOS 的/Library
的空間佔用 9.02GB, 文件數只是前者的兩倍(67,890). 綜上能夠看出 node_modules 的特色是:webpack
因此說 node_modules 對於機械硬盤來講是個噩夢, 記得有一次一個同事刪除 node_modules 一個下午都沒搞定. 對於前端開發者來講, 咱們有 N 個須要npm install
的項目 😹.git
除此以外, Node 的模塊機制還有如下缺點:github
Node 自己並無模塊的概念, 它在運行時進行查找和加載. 這個缺點和*'動態語言與靜態語言的優劣對比'*類似, 你可能在開發環境運行得好好的, 可能到了線上就運行不了了, 緣由是一個模塊沒有添加到 package.jsonweb
Node 模塊的查找策略很是浪費. 這個缺點在大部分前端項目中能夠進行優化, 好比 webpack 就能夠限定只在項目根目錄下的 node_modules 中查找, 可是對於嵌套的依賴, 依然須要 2 次以上的查找
node_modules 不能有效地處理重複的包. 兩個名稱相同可是不一樣版本的包是不能在一個目錄下共存的. 因此會致使嵌套的 node_modules, 並且這些項目'依賴的依賴'是沒法和項目或其餘依賴共享的:
# ① 假設項目依賴a,b,c三個模塊, 依賴樹爲:
# +- a
# +- react@15
# +- b
# +- react@16
# +- c
# +- react@16
# yarn安裝時會按照項目被依賴的次數做爲權重, 將依賴提高(hoisting),
# 安裝後的node_modules結構爲:
.
└── node_modules
├── a
│ ├── index.js
│ ├── node_modules
│ │ └── react # @15
│ └── package.json
├── b
│ ├── index.js
│ └── package.json
├── c
│ ├── index.js
│ └── package.json
└── react # @16 被依賴了兩次, 因此進行提高
# ② 如今假設在①的基礎上, 根項目依賴了react@15, 對於項目本身的依賴確定是要放在node_modules根目錄的,
# 因爲一個目錄下不能存在同名目錄, 因此react@16沒有的提高機會.
# 安裝後node_moduels結構爲
.
└── node_modules
├── a
│ ├── index.js
│ └── package.json # react@15 提高
├── b
│ ├── index.js
│ ├── node_modules
│ │ └── react # @16
│ └── package.json
├── c
│ ├── index.js
│ ├── node_modules
│ │ └── react # @16
│ └── package.json
└── react # @15
# 上面的結果能夠看出, react@16出現了重複
複製代碼
爲此 Yarn 集成了Plug'n'Play
(簡稱 pnp), 中文名稱能夠稱爲'即插即用', 來解決 node_modules'地獄'.
按照普通的按照流程, Yarn 會生成一個 node_modules 目錄, 而後 Node 按照它的模塊查找規則在 node_modules 目錄中查找. 但實際上 Node 並不知道這個模塊是什麼, 它在 node_modules 查找, 沒找到就在父目錄的 node_modules 查找, 以此類推. 這個效率是很是低下的.
可是 Yarn 做爲一個包管理器, 它知道你的項目的依賴樹. 那能不能讓 Yarn 告訴 Node? 讓它直接到某個目錄去加載模塊. 這樣便可以提升 Node 模塊的查找效率, 也能夠減小 node_modules 文件的拷貝. 這就是Plug'n'Play
的基本原理.
在 pnp 模式下, Yarn 不會建立 node_modules 目錄, 取而代之的是一個.png.js
文件, 這是一個 node 程序, 這個文件包含了項目的依賴樹信息, 模塊查找算法, 也包含了模塊查找器的 patch 代碼(在 Node 環境, 覆蓋 Module._load 方法).
使用 pnp 機制的如下優勢:
yarn install
節省 70%的時間在 Mac 下 Yarn 的安裝速度很是快, 熱緩存下僅需幾秒. 緣由是 SSD + APFS 的 Copy-on-write 機制. 這使得文件的拷貝不用佔用空間, 至關於建立一個連接. 因此拷貝和刪除的速度很是快. 可是 node_modules 複雜的目錄結構和超多的文件, 仍然須要調用大量的系統調用, 這也會拖慢安裝過程.
💡 若是以爲 pnp 繁瑣或不可靠, 那就趕忙用上 SSD 配合支持 Copy-on-write 的文件系統.
使用 pnp 的風險:
目前前端社區的各類工具都依賴於 node_modules 模塊查找機制. 例如
pnp 一個很是新的東西, 在去年 9 月份(2018)面世. 要讓這些工具和 pnp 集成是個不小的挑戰, 並且這些這些工具 和 pnp 都是在不斷迭代的, pnp 還不穩定, 將來可能變化, 這也會帶來某些維護方面的負擔.
除了模塊查找機制, 有一些工具是直接在 node_modules 中作其餘事情的, 好比緩存, 存放臨時證書. 例如cache-loader
, webpack-dev-server
若是隻是單純的 Node 項目, 遷入過程還算比較簡單. 首先在package.json
開啓 pnp 安裝模式:
{
"installConfig": {
"pnp": true
}
}
複製代碼
接着安裝依賴:
yarn add express
複製代碼
安裝後項目根目錄就會出現一個.pnp.js
文件. 下一步編寫代碼:
// index.js
const express = require('express');
const app = express();
const port = 3000;
app.get('/', (req, res) => res.send('Hello World!'));
app.listen(port, () => console.log(`Example app listening on port ${port}!`));
複製代碼
接下來就是運行 Node 代碼了, 若是直接node index.js
會報Error: Cannot find module 'express'
異常. 這是由於尚未 patch Node 的模塊查找器. 能夠經過如下命令運行:
yarn node
# 或者
node --require="./.pnp.js" index.js
複製代碼
.pnp.js
文件不該該提交到版本庫, 這個文件裏面包含了硬編碼的緩存目錄. 在 Yarn v2 中會進行重構
pnp 集成無非就是從新實現現有工具的模塊查找機制. 隨着前端工程化的發展, 一個前端項目會集成很是多的工具, 若是這些工具無法適配, 能夠說 pnp 很難往前走. 然而這並非 pnp 可以控制的, 須要這些工具開發者的配合.
社區上很多項目已經集成了 pnp:
對於 Node, pnp 是開箱即用的, 直接使用--require="./.pnp.js"
導入.pnp.js
文件便可, .pnp.js
會對 Node 的 Module 對象進行 patch, 從新實現模塊查找機制
Webpack 使用的模塊查找器是enhanced-resolve
, 能夠經過pnp-webpack-plugin
插件來擴展enhanced-resolve
來支持 pnp.
const PnpWebpackPlugin = require(`pnp-webpack-plugin`);
module.exports = {
resolve: {
// 擴展模塊查找器
plugins: [PnpWebpackPlugin],
},
resolveLoader: {
// 擴展loader模塊查找器.
plugins: [PnpWebpackPlugin.moduleLoader(module)],
},
};
複製代碼
jest支持經過resolver
來配置查找器:
module.exports = {
resolver: require.resolve(`jest-pnp-resolver`),
};
複製代碼
Typescript 也使用本身的模塊查找器, TS團隊爲了性能方面的考慮, 暫時不容許第三方工具來擴展查找器. 也就是說暫時不能用.
在這個issue中, 有人提出使用"moduleResolution": "yarnpnp"
或者使用相似ts-loader
的resolveModuleName
的方式支持 pnp 模塊查找.
TS 團隊的迴應是: pnp(或者 npm 的 tink)仍是早期階段, 將來可能會有變化, 例如.pnp.js
文件, 顯然不合適那麼早入坑. 另外爲了優化和控制編譯器性能, TS 也沒有計劃在編譯期間暴露接口給第三方執行代碼.
因此如今 Typescript 至今也沒有相似 babel 的插件機制. 除非本身實現一個'TS compiler host', 例如ts-loader
就本身擴展了插件機制和模塊查找機制, 來支持相似ts-import-plugin等插件, 所以ts-loader
如今是支持 pnp 的:
const PnpWebpackPlugin = require(`pnp-webpack-plugin`);
module.exports = {
module: {
rules: [
{
test: /\.ts$/,
loader: require.resolve('ts-loader'),
options: PnpWebpackPlugin.tsLoaderOptions(),
},
],
},
};
複製代碼
總結, Typescript
暫時不支持, 且近期也沒有開發計劃, 因此VsCode
也別期望了. fork-ts-checker-webpack-plugin
也還沒跟上. 顯然 Typescript 是 pnp 的第一攔路虎
綜上, pnp 是一個不錯的解決方案, 能夠解決 Node 模塊機制的空間和時間的效率問題. 可是在現階段, 它還不是成熟, 有 不少坑要踩, 且和社區各類工具集成存在很多問題. 因此還不建議在生產環境中使用.
因此目前階段對於普通開發者來講, 若是要提高npm安裝速度, 仍是得上SSD+Copy-On-Write!😂
下面是各類項目的集成狀況(✅(支持)|🚧(計劃中或不完美)|❌(不支持)):
項目 | |
---|---|
Webpack | ✅ |
rollup | ✅ |
browserify | ✅ |
gulp | ✅ |
jest | ✅ |
Node | ✅ |
Typescript/VScode IntelliSense | ❌ |
eslint | 🚧 |
flow | 🚧 |
create-react-app | 🚧 |
ts-loader | ✅ |
fork-ts-checker-webpack-plugin | 🚧 |
相關 issues:
.pnp.js
yarn-pnp
其餘方案