Yarn Plug'n'Play能否助你脫離node_modules苦海?

使用 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

frontend-project

而 macOS 的/Library目錄的大小的文件數:react

macos library

一行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 機制的如下優勢:

  • 擺脫 node_modules.
    • 時間上: 相比較在熱緩存(hot cache)環境下運行yarn install節省 70%的時間
    • 空間上: pnp 模式下, 全部 npm 模塊都會存放在全局的緩存目錄下, 依賴樹扁平化, 避免拷貝和重複
  • 提升模塊加載效率. Node 爲了查找模塊, 須要調用大量的 stat 和 readdir 系統調用. pnp 經過 Yarn 獲取或者模塊信息, 直接定位模塊
  • 再也不受限於 node_modules 同名模塊不一樣版本不能在同一目錄

在 Mac 下 Yarn 的安裝速度很是快, 熱緩存下僅需幾秒. 緣由是 SSD + APFS 的 Copy-on-write 機制. 這使得文件的拷貝不用佔用空間, 至關於建立一個連接. 因此拷貝和刪除的速度很是快. 可是 node_modules 複雜的目錄結構和超多的文件, 仍然須要調用大量的系統調用, 這也會拖慢安裝過程.
💡 若是以爲 pnp 繁瑣或不可靠, 那就趕忙用上 SSD 配合支持 Copy-on-write 的文件系統.


使用 pnp 的風險:

目前前端社區的各類工具都依賴於 node_modules 模塊查找機制. 例如

  • Node
  • Electron, electron-builder 等等
  • Webpack
  • Typescript: 定位類型聲明文件
  • Babel: 定位插件和 preset
  • Eslint: 定位插件和 preset, rules
  • Jest
  • 編輯器, 如 VsCode
  • ...😿

pnp 一個很是新的東西, 在去年 9 月份(2018)面世. 要讓這些工具和 pnp 集成是個不小的挑戰, 並且這些這些工具 和 pnp 都是在不斷迭代的, pnp 還不穩定, 將來可能變化, 這也會帶來某些維護方面的負擔.

除了模塊查找機制, 有一些工具是直接在 node_modules 中作其餘事情的, 好比緩存, 存放臨時證書. 例如cache-loader, webpack-dev-server


開啓 pnp

若是隻是單純的 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

對於 Node, pnp 是開箱即用的, 直接使用--require="./.pnp.js"導入.pnp.js文件便可, .pnp.js會對 Node 的 Module 對象進行 patch, 從新實現模塊查找機制

Webpack

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

jest支持經過resolver來配置查找器:

module.exports = {
  resolver: require.resolve(`jest-pnp-resolver`),
};
複製代碼

Typescript

Typescript 也使用本身的模塊查找器, TS團隊爲了性能方面的考慮, 暫時不容許第三方工具來擴展查找器. 也就是說暫時不能用.

在這個issue中, 有人提出使用"moduleResolution": "yarnpnp"或者使用相似ts-loaderresolveModuleName的方式支持 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:

其餘方案

  • npm tink: a dependency unwinder for javascript
  • pnpm Fast, disk space efficient package manager
  • Yarn Workspaces 多個項目共有依賴
相關文章
相關標籤/搜索