一個爲了讓console.log寫起來更偷懶的webpack-plugin

做爲一個敲碼5分鐘,調試兩小時的bug大叔,天天和console.log打的交道天然很多,人到中年,愈來愈懶,因而想把console.log('bug: ', bug)變成log.bug來讓個人懶癌病發得更加完全。因而硬着頭皮看了下webpack插件的寫法,在此記錄一下webpack系統的學習筆記。webpack

跟着webpack源碼摸石過河

去吧!皮卡丘!!!git

  1. webpack初始化
// webpack.config.js

const webpack = require('webpack');
const WebpackPlugin = require('webpack-plugin');

const options = {
  entry: 'index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'index.bundle.js'
  },
  module: {
    rules: []
  },
  plugins: [
    new WebpackPlugin()
  ]
  // ...
};

webpack(options);
複製代碼
// webpack.js

// 引入Compiler類
const Compiler = require("./Compiler");

// webpack入口函數
const webpack = (options, callback) => {

  // 建立一個Compiler實例
  compiler = new Compiler(options.context);
  
  // 實例保存webpack的配置對象
  compiler.options = options
  
  // 依次調用webpack插件的apply方法,並傳入compiler引用
  for (const plugin of options.plugins) {
    plugin.apply(compiler);
  }
  
  //執行實例的run方法
  compiler.run(callback)
  
  //返回實例
  return compiler
}

module.exports = webpack
複製代碼
  • 最開始,不管咱們在控制檯輸入webpack指令仍是使用Node.js的API,都是調用了webpack函數(源碼),並傳入了webpack的配置選項,建立了一個 compiler 實例。
  • compiler 是什麼?—— 明顯發現compiler保存了完整的webpack的配置參數options。因此官方說:

compiler 對象表明了完整的 webpack 環境配置。這個對象在啓動 webpack 時被一次性創建,並配置好全部可操做的設置,包括 options,loader 和 plugin。可使用 compiler 來訪問 webpack 的主環境。github

  • 全部 webpack-plugin 也在這裏經過提供一個叫 apply 的方法給webpack調用,以完成初始化的工做,而且接收到剛建立的 compiler 的引用。
  1. compiler、compilation與tapable
// Compiler.js

const {
  Tapable,
  SyncHook,
  SyncBailHook,
  AsyncParallelHook,
  AsyncSeriesHook
} = require("tapable");

const Compilation = require("./Compilation");

class Compiler extends Tapable {

  hooks = [
    hook1: new SyncHook(),
    hook2: new AsyncSeriesHook(),
    hook3: new AsyncParallelHook()
  ]

  run () {
    // ...
    // 觸發鉤子回調執行
    this.hooks[hook1].call()
    this.hooks[hook2].callAsync(() => {
      this.hooks[hook4].call()
    })

    // 進入編譯流程
    this.compile()
    // ...
    this.hooks[hook3].promise(() => {})
  }

  compile () {
    // 建立一個Compilation實例
    const compilation = new Compilation(this)
  }
}
複製代碼
  • 研究下Compiler.js這個文件,它引入了一個 tapable 類(源碼)的庫,這個庫提供了一些 Hook 類,內部實現了相似的 事件訂閱/發佈系統web

  • 哪裏用到 Hook 類?—— 在 Compiler 類(Compiler.js源碼) 裏擁有不少有意思的hook,這些hook表明了整個編譯過程的各個關鍵事件節點,它們都是繼承於 Hook 類 ,因此都支持 監聽/訂閱,只要咱們的插件提早作好事件訂閱,那麼編譯流程進行到該事件點時,就會執行咱們提供的 事件回調 ,作咱們想作的事情了。express

  • 如何進行訂閱呢?——在上文中每一個webpack-plugin中都有一個apply方法。其實註冊的代碼就藏在裏面,經過如下相似的代碼實現。任何的webpack插件均可以訂閱hook1,所以hook1維護了一個taps數組,保存着全部的callback。npm

    compiler.hooks.hook1.tap(name, callback) // 註冊/訂閱api

    compiler.hooks.hook1.call() // 觸發/發佈數組

  • 準備工做作好了以後,當 run() 方法開始被調用時,編譯就正式開始了。在該方法裏,執行 call/callAsync/promise 這些事件時(他們由webpack內部包括一些官方使用的webpack插件進行觸發的管理,無需開發者操心),相應的hook就會把本身的taps裏的函數均執行一遍。大概的邏輯以下所示。 promise

  • 其中,hooks的執行是按照編譯的流程順序來的,hooks之間有彼此依賴的關係。bash

  • compilation 實例也在這裏建立了,它表明了一次資源版本構建。每當檢測到文件變化,就會建立一個新的 compilation,從而生成一組新的編譯資源。一個 compilation 對象表現了當前的模塊資源、編譯生成資源、變化的文件、以及被跟蹤依賴的狀態信息。並且compilation也和compiler相似,擁有不少hooks(Compilation.js源碼),所以一樣提供了不少事件節點給予咱們訂閱使用。

  1. webpack插件體系的使用套路
class MyPlugin {
  apply(compiler) {
    // 設置回調來訪問 compilation 對象:
    compiler.hooks.compilation.tap('myPlugin', (compilation) => {
      // 如今,設置回調來訪問 compilation 中的任務點:
      compilation.hooks.optimize.tap('myPlugin', () => {
        console.log('Hello compilation!');
      });
    });
  }
}

module.exports = MyPlugin;
複製代碼
  • 訂閱 compilercomplation 的事件節點都在webpack-plugin中的 apply 方法裏書寫,具體的演示如上。固然你想拿到編譯過程當中的什麼資源,首先得要找出能提供該資源引用的對應的compiler事件節點進行訂閱(上帝先創造了亞當,再有夏娃)。每一個compiler時間節點能提供什麼參數,在hook的實例化時已經作了說明(以下),更多可查看源碼
this.hooks = {
  // 拿到compilation和params
  compilation: new SyncHook(["compilation", "params"]),
  // 拿到stats
  done: new AsyncSeriesHook(["stats"]),
  // 拿到compiler
  beforeRun: new AsyncSeriesHook(["compiler"])
}
複製代碼

梳理webpack流程

通過以上的初步探索,寫webpack插件須要瞭解的幾個知識點應該有了大概的掌握:

  1. 插件提供 apply 方法供 webpack 調用進行初始化
  2. 使用 tap 註冊方式鉤入 compilercompilation 的編譯流程
  3. 使用 webpack 提供的 api 進行資源的個性化處理。

寫插件的套路已經知道了,如今還剩如何找出合適的鉤子,修改資源這件事。在webpack系統裏,鉤子即流程,是編譯構建工做的生命週期 。固然,想要了解全部 tapable 實例對象的鉤子的具體做用,須要探索webpack全部的內部插件如何使用這些鉤子,作了什麼工做來進行總結,想一想就複雜,因此只能抽取重要流程作思路歸納,借用淘寶的一張經典圖示。![webpack_flow.jpg](file:///Users/Ando/Documents/webpack-plugin/webpack_flow.jpg) 整個編譯流程大概分紅三個階段,如今從新整理一下:

  1. 準備階段 ,webpack的初始化
  • webpack依次調用開發者自定義的插件的 apply 方法,讓插件們作好事件註冊。
  • WebpackOptionsApply 接收組合了 命令行webpack.config.js 的配置參數,負責webpack內部基礎流程使用的插件和compiler上下文環境的初始化工做。(*Plugin均爲webpack內部使用的插件)
    // webpack.js
      // 開發者自定義的插件初始化
      if (options.plugins && Array.isArray(options.plugins)) {
        for (const plugin of options.plugins) {
          if (typeof plugin === "function") {
            plugin.call(compiler, compiler)
          } else {
            plugin.apply(compiler)
          }
        }
      }
      // ...
      // webpack內部使用的插件初始化
      compiler.options = new WebpackOptionsApply().process(options, compiler)
    
      // WebpackOptionsApply.js
      class WebpackOptionsApply extends OptionsApply {
        process (options, compiler) {
          // ...
          new WebAssemblyModulesPlugin({
              mangleImports: options.optimization.mangleWasmImports
          }).apply(compiler);
    
          new EntryOptionPlugin().apply(compiler);
          compiler.hooks.entryOption.call(options.context, options.entry);
    
          new CompatibilityPlugin().apply(compiler);
          new HarmonyModulesPlugin(options.module).apply(compiler);
          new AMDPlugin(options.module, options.amd || {}).apply(compiler);
          // ...
        }
      }
    複製代碼
  • 執行 run / watch(一次打包/監聽打包模式)觸發 編譯 階段
  1. 編譯階段,生成modulechunk資源。

    runcompile 編譯 → 建立 compilation 對象。compilation 的建立是一次編譯的起步,即將對全部模塊加載(load)封存(seal)優化(optimiz)分塊(chunk)哈希(hash)從新建立(restore)

    module.exports = class Compiler extends Tapable {
    
        run () {
          // 聲明編譯結束回調
          function onCompiled () {}
          // 觸發run鉤子
          this.hooks.run.callAsync(this, err => {
            this.compile(onCompiled)
          })
        }
    
        compile(callback) {
          // ...
          // 編譯開始前,觸發beforeCompile鉤子
          this.hooks.beforeCompile.callAsync(params, err => {
            // 編譯開始,觸發compile鉤子
            this.hooks.compile.call(params);
            // 建立compilation實例
            const compilation = this.newCompilation(params);
            // 觸發make鉤子
            this.hooks.make.callAsync(compilation, err => {
              // 模塊解析完畢,執行compilation的finish方法
              compilation.finish();
              // 資源封存,執行seal方法
              compilation.seal(err => {
                // 編譯結束,執行afterCompile鉤子
                this.hooks.afterCompile.callAsync(compilation, err => {
    
                  // ...
                });
              });
            });
          });
        }
      }
    複製代碼
  • 加載模塊make鉤子觸發 → DllEntryPlugin 內部插件調用compilation.addEntrycompilation維護了一些資源生成工廠方法 compilation.dependencyFactories ,負責把入口文件及其(循環)依賴轉換成 modulemodule 的解析過程會應用匹配的 loader )。每一個 module 的解析過程提供 buildModule / succeedModule 等鉤子, 全部 module 解析完成後觸發 finishModules 鉤子。
  • 封存seal 方法包含了 優化/分塊/哈希 , 編譯中止接收新模塊,開始生成chunks。此階段依賴了一些webpack內部插件對module進行優化,爲本次構建生成的chunk加入hash等。createChunkAssets()會根據chunk類型使用不一樣的模板進行渲染。此階段執行完畢後就表明一次編譯完成,觸發 afterCompile鉤子
    • 優化BannerPlugin
      compilation.hooks.optimizeChunkAssets.tapAsync('MyPlugin', (chunks, callback) => {
          chunks.forEach(chunk => {
        	chunk.files.forEach(file => {
        	  compilation.assets[file] = new ConcatSource(
        	    '\/**Sweet Banner**\/',
        	    '\n',
        	    compilation.assets[file]
        	  );
        	});
          });
      
          callback();
        });
      複製代碼
    • 分塊:用來分割chunk的 SplitChunksPlugin 插件監聽了optimizeChunksAdvanced鉤子
    • 哈希createHash
  1. 文件生成階段
  • 編譯完成後,觸發 emit ,遍歷 compilation.assets 生成全部文件。

寫一個加強console.log調試體驗的webpack插件 simple-log-webpack-plugin

一張效果圖先上爲敬。(對照圖片)只需寫 log.a ,經過本身的webpack插件自動補全字段標識 a字段: ,加入 文件路徑 ,輕鬆支持 打印顏色 效果,相同文件的日誌信息 可摺疊 ,給你一個簡潔方便的調試環境。

如下爲源碼,歡迎測試反饋。github npm

const ColorHash = require('color-hash')
const colorHash = new ColorHash()

const Dependency = require('webpack/lib/Dependency');

class LogDependency extends Dependency {

  constructor(module) {
    super();
    this.module = module;
  }
}

LogDependency.Template = class {
  apply(dep, source) {

    const before = `;console.group('${source._source._name}');`
    const after = `;console.groupEnd();`
    const _size = source.size()

    source.insert(0, before)

    source.replace(_size, _size, after)

  }
};


module.exports = class LogPlugin {

  constructor (opts) {
    this.options = {
      expression: /\blog\.(\w+)\b/ig,
      ...opts
    }
    this.plugin = { name: 'LogPlugin' }
    
  }

  doLog (module) {

    if (!module._source) return
    let _val = module._source.source(),
        _name = module.resource;

    const filedColor = colorHash.hex(module._buildHash)

    // 判斷是否須要加入
    if (this.options.expression.test(_val)) {
      module.addDependency(new LogDependency(module));
    }

    _val = _val.replace(
      this.options.expression,
      `console.log('%c$1字段:%c%o, %c%s', 'color: ${filedColor}', 'color: red', $1, 'color: pink', '${_name}')`
    )

    return _val

  }

  apply (compiler) {

    compiler.hooks.compilation.tap(this.plugin, (compilation) => {

      // 註冊自定義依賴模板
      compilation.dependencyTemplates.set(
        LogDependency,
        new LogDependency.Template()
      );

      // modlue解析完畢鉤子
      compilation.hooks.succeedModule.tap(this.plugin, module => {
        // 修改模塊的代碼
        module._source._value = this.doLog(module)
        
      })
    })

  }
}
複製代碼
相關文章
相關標籤/搜索