1. 程式人生 > >webpack-tapable-0.2.8 原始碼分析

webpack-tapable-0.2.8 原始碼分析

webpack 是基於事件流的打包構建工具,也就是內建了很多 hooks。作為使用方,可以在這些鉤子當中,去插入自己的處理邏輯,而這一切的實現都得益於 tapable 這個工具。它有多個版本,webpack 前期的版本是依賴於 tapable 0.2.8 這個版本,後來重構了,發了 2.0.0 beta 版本,因為原始碼都是通過字串拼接,通過 new Function 的模式使用,所以看起來比較晦澀。

那麼既然如此,我們先從早期的 0.2.8 這個版本瞭解下它的前身,畢竟核心思想不會發生太大的變化。

tapable 的實現類似於 node 的 EventEmitter 的釋出訂閱模式。用一個物件的鍵儲存對應的事件名稱,鍵值來儲存事件的處理函式,類似於下面:

function Tapable () {
  this._plugins = {
    'emit': [handler1, handler2, ......]
  }
}
複製程式碼

同時,原型上定義了不同的方法來呼叫 handlers。

我們先來看下用法,

  1. pluginapplyPlugins

    void plugin(names: string|string[], handler: Function)
    void applyPlugins(name: string, args: any...)
    複製程式碼

    最基礎的就是註冊外掛以及外掛觸發的回撥函式。

    const Tapable = require
    ('tapable') const t = new Tapable() // 註冊外掛 t.plugin('emit', (...args) => { console.log(args) console.log('This is a emit handler') }) // 呼叫外掛 t.applyPlugins('emit', '引數1') // 列印如下 [ '引數1' ] This is a emit handler 複製程式碼

    原始碼如下

    Tapable.prototype.applyPlugins = function applyPlugins(name) {
      if(!this
    ._plugins[name]) return; var args = Array.prototype.slice.call(arguments, 1); var plugins = this._plugins[name]; for(var i = 0; i < plugins.length; i++) plugins[i].apply(this, args); }; Tapable.prototype.plugin = function plugin(name, fn) { if(Array.isArray(name)) { name.forEach(function(name) { this.plugin(name, fn); }, this); return; } if(!this._plugins[name]) this._plugins[name] = [fn]; else this._plugins[name].push(fn); }; 複製程式碼

    很簡單,內部維護 _plugins 屬性來快取 plugin 名稱以及 handler。

  2. apply

    void apply(plugins: Plugin...)
    複製程式碼

    接收 plugin 作為引數,每個 plugin 必須提供 apply 方法,也就是 webpack 在編寫 plugin 的規是外掛例項必須提供 apply 方法。

    const Tapable = require('tapable')
    const t = new Tapable()
    
    // 宣告一個 webpack 外掛的類,物件必須宣告 apply 方法
    class WebpackPlugin {
      constructor () {}
      apply () {
        console.log('This is webpackPlugin')
      }
    }
    
    const plugin = new WebpackPlugin()
    
    // tapable.apply
    t.apply(plugin) // print 'This is webpackPlugin'
    複製程式碼

    原始碼如下

    Tapable.prototype.apply = function apply() {
      for(var i = 0; i < arguments.length; i++) {
        arguments[i].apply(this);
      }
    };
    複製程式碼

    也很簡單,依次執行每個外掛的 apply 方法。

  3. applyPluginsWaterfall

    any applyPluginsWaterfall(name: string, init: any, args: any...)
    複製程式碼

    依次呼叫外掛對應的 handler,傳入的引數是上一個 handler 的返回值,以及呼叫 applyPluginsWaterfall 傳入 args 引數組成的陣列,說起來很繞,看看下面的例子:

    t.plugin('waterfall', (...args) => {
      // print ['init', 'args1']
      console.log(args)
      return 'result1'
    })
    
    t.plugin('waterfall', (...args) => {
      // print ['result1', 'args1']
      console.log(args)
      return 'result2'
    })
    
    const ret = t.applyPluginsWaterfall('waterfall', 'init', 'args1') // ret => 'result2'
    複製程式碼

    原始碼如下

    Tapable.prototype.applyPluginsWaterfall = function applyPluginsWaterfall(name, init) {
      if(!this._plugins[name]) return init;
      var args = Array.prototype.slice.call(arguments, 1);
      var plugins = this._plugins[name];
      var current = init;
      for(var i = 0; i < plugins.length; i++) {
        args[0] = current;
        current = plugins[i].apply(this, args);
      }
      return current;
    };
    複製程式碼

    上一個 handler 返回的值,會作為下一個 handler的第一個引數。

  4. applyPluginsBailResult

    any applyPluginsBailResult(name: string, args: any...)
    複製程式碼

    依次呼叫外掛對應的 handler,傳入的引數是 args,如果正執行的 handler 的 返回值不是 undefined,其餘的 handler 都不會執行了。 bail 是保險的意思,即只要任意一個 handler 有 !== undefined 的返回值,那麼函式的執行就終止了。

    t.plugin('bailResult', (...args) => {
      // [ '引數1', '引數2' ]
      console.log(args)
      return 'result1'
    })
    
    t.plugin('bailResult', (...args) => {
      // 因為上一個函式返回了 'result1',所以不會執行到這個handler
      console.log(args)
      return 'result2'
    })
    
    t.applyPluginsBailResult('bailResult', '引數1', '引數2')
    複製程式碼

    原始碼如下

    Tapable.prototype.applyPluginsBailResult = function applyPluginsBailResult(name, init) {
      if(!this._plugins[name]) return;
      var args = Array.prototype.slice.call(arguments, 1);
      var plugins = this._plugins[name];
      for(var i = 0; i < plugins.length; i++) {
        var result = plugins[i].apply(this, args);
        if(typeof result !== "undefined") {
          return result;
        }
      }
    };
    複製程式碼

    只要 handler 返回的值 !== undefined,就會停止呼叫接下來的 handler。

  5. applyPluginsAsyncSeries & applyPluginsAsync(支援非同步)

    void applyPluginsAsync(
      name: string,
      args: any...,
      callback: (err?: Error) -> void
    )
    複製程式碼

    applyPluginsAsyncSeries 與 applyPluginsAsync 的函式引用都是相同的,並且函式內部支援非同步。callback 在所有 handler 都執行完了才會呼叫,但是在註冊 handler 的時候,函式內部一定要執行 next() 的邏輯,這樣才能執行到下一個 handler。

    t.plugin('asyncSeries', (...args) => {
      // handler 的最後一個引數一定是 next 函式
      const next = args.pop()
      // 執行 next,函式才會執行到下面的 handler
      setTimeout (() => {
        next()
      }, 3000)
    })
    
    t.plugin('asyncSeries', (...args) => {
      // handler 的最後一個引數一定是 next
      const callback = args.pop()
      // 執行 next,函式才會執行到 applyPluginsAsyncSeries 傳入的 callback
      Promise.resolve(1).then(next)
    })
    
    t.applyPluginsAsyncSeries('asyncSeries', '引數1', (...args) => {
      console.log('這是 applyPluginsAsyncSeries 的 callback')
    })
    複製程式碼

    原始碼如下

    Tapable.prototype.applyPluginsAsyncSeries = Tapable.prototype.applyPluginsAsync = function applyPluginsAsyncSeries(name) {
      var args = Array.prototype.slice.call(arguments, 1);
      var callback = args.pop();
      var plugins = this._plugins[name];
      if(!plugins || plugins.length === 0) return callback();
      var i = 0;
      var _this = this;
      args.push(copyProperties(callback, function next(err) {
        if(err) return callback(err);
        i++;
        if(i >= plugins.length) {
          return callback();
        }
        plugins[i].apply(_this, args);
      }));
      plugins[0].apply(this, args);
    };
    複製程式碼

    applyPluginsAsyncSeries 內部維護了一個 next 函式,這個函式作為每個 handler 的最後一個引數傳入,handler 內部支援非同步操作,但是必須手動呼叫 next 函式,才能執行到下一個 handler。

  6. applyPluginsAsyncSeriesBailResult(支援非同步)

    void applyPluginsAsyncSeriesBailResult(
      name: string,
      args: any...,
      callback: (result: any) -> void
    )
    複製程式碼

    函式支援非同步,只要在 handler 裡面呼叫 next 回撥函式,並且傳入任意引數,就會直接執行 callback。

    t.plugin('asyncSeriesBailResult', (...args) => {
      // handler 的最後一個引數一定是 next 函式
      const next = args.pop()
      // 因為傳了字串,導致直接執行 callback
      next('跳過 handler 函式')
    })
    
    t.plugin('asyncSeriesBailResult', (...args) => {
      
    })
    
    t.applyPluginsAsyncSeriesBailResult('asyncSeriesBailResult', '引數1', (...args) => {
      console.log('這是 applyPluginsAsyncSeriesBailResult 的 callback')
    })
    // print '這是 applyPluginsAsyncSeriesBailResult 的 callback'
    複製程式碼

    原始碼如下

    Tapable.prototype.applyPluginsAsyncSeriesBailResult = function applyPluginsAsyncSeriesBailResult(name) {
      var args = Array.prototype.slice.call(arguments, 1);
      var callback = args.pop();
      if(!this._plugins[name] || this._plugins[name].length === 0) return callback();
      var plugins = this._plugins[name];
      var i = 0;
      var _this = this;
      args.push(copyProperties(callback, function next() {
        if(arguments.length > 0) return callback.apply(null, arguments);
        i++;
        if(i >= plugins.length) {
          return callback();
        }
        plugins[i].apply(_this, args);
      }));
      plugins[0].apply(this, args);
    };
    複製程式碼

    applyPluginsAsyncSeriesBailResult 內部維護了一個 next 函式,這個函式作為每個 handler 的最後一個引數傳入,handler 內部支援非同步操作,但是必須手動呼叫 next 函式,才能執行到下一個 handler,next 函式可以傳入引數,這樣會直接執行 callback。

  7. applyPluginsAsyncWaterfall(支援非同步)

    void applyPluginsAsyncWaterfall(
      name: string,
      init: any,
      callback: (err: Error, result: any) -> void
    )
    複製程式碼

    函式支援非同步,handler 的接收兩個引數,第一個引數是上一個 handler 通過 next 函式傳過來的 value,第二個引數是 next 函式。next 函式接收兩個引數,第一個是 error,如果 error 存在,就直接執行 callback。第二個 value 引數,是傳給下一個 handler 的引數。

    t.plugin('asyncWaterfall', (value, next) => {
    // handler 的最後一個引數一定是 next 函式
      console.log(value)
      next(null, '來自第一個 handler')
    })
    
    t.plugin('asyncWaterfall', (value, next) => {
      console.log(value)
      next(null, '來自第二個 handler')
    })
    
    t.applyPluginsAsyncWaterfall('asyncWaterfall', '引數1', (err, value) => {
      if (!err) {
        console.log(value)
      }
    })
    
    // 列印如下
    
    引數1
    來自第一個 handler
    來自第二個 handler
    複製程式碼

    原始碼如下

    Tapable.prototype.applyPluginsAsyncWaterfall = function applyPluginsAsyncWaterfall(name, init, callback) {
      if(!this._plugins[name] || this._plugins[name].length === 0) return callback(null, init);
      var plugins = this._plugins[name];
      var i = 0;
      var _this = this;
      var next = copyProperties(callback, function(err, value) {
        if(err) return callback(err);
        i++;
        if(i >= plugins.length) {
          return callback(null, value);
        }
        plugins[i].call(_this, value, next);
      });
      plugins[0].call(this, init, next);
    };
    複製程式碼

    applyPluginsAsyncWaterfall 內部維護了一個 next 函式,這個函式作為每個 handler 的最後一個引數傳入,handler 內部支援非同步操作,但是必須手動呼叫 next 函式,才能執行到下一個 handler,next 函式可以傳入引數,第一個引數為 err, 第二引數為上一個 handler 返回值。

  8. applyPluginsParallel(支援非同步)

    void applyPluginsParallel(
      name: string,
      args: any...,
      callback: (err?: Error) -> void
    )
    複製程式碼

    並行的執行函式,每個 handler 的最後一個引數都是 next 函式,這個函式用來檢驗當前的 handler 是否已經執行完。

    t.plugin('parallel', (...args) => {
      const next = args.pop()
      console.log(1)
      // 必須呼叫 next 函式,要不然 applyPluginsParallel 的 callback 永遠也不會回撥
      next('丟擲錯誤了1', '來自第一個 handler')
    })
    
    t.plugin('parallel', (...args) => {
      const next = args.pop()
      console.log(2)
      // 必須呼叫 next 函式,要不然 applyPluginsParallel 的 callback 永遠也不會回撥
      next('丟擲錯誤了2')
    })
    
    t.applyPluginsParallel('parallel', '引數1', (err) => {
      // print '丟擲錯誤了1'
      console.log(err)
    })
    複製程式碼

    原始碼如下

    Tapable.prototype.applyPluginsParallel = function applyPluginsParallel(name) {
      var args = Array.prototype.slice.call(arguments, 1);
      var callback = args.pop();
      if(!this._plugins[name] || this._plugins[name].length === 0) return callback();
      var plugins = this._plugins[name];
      var remaining = plugins.length;
      args.push(copyProperties(callback, function(err) {
        if(remaining < 0) return; // ignore
        if(err) {
          remaining = -1;
          return callback(err);
        }
        remaining--;
        if(remaining === 0) {
          return callback();
        }
      }));
      for(var i = 0; i < plugins.length; i++) {
        plugins[i].apply(this, args);
        if(remaining < 0) return;
      }
    };
    複製程式碼

    applyPluginsParallel 並行地呼叫 handler。內部通過閉包維護了 remaining 變數,用來判斷內部的函式是否真正執行完,handler 的最後一個引數是一個函式 check。如果 handler 內部使用者想要的邏輯執行完,必須呼叫 check 函式來告訴 tapable,進而才會執行 args 陣列的最後一個 check 函式。

  9. ** applyPluginsParallelBailResult **(支援非同步)

    void applyPluginsParallelBailResult(
      name: string,
      args: any...,
      callback: (err: Error, result: any) -> void
    )
    複製程式碼

    並行的執行函式,每個 handler 的最後一個引數都是 next 函式,next 函式必須呼叫,如果給 next 函式傳參,會直接走到 callback 的邏輯。callback 執行的時機是跟 handler 註冊的順序有關,而不是跟 handler 內部呼叫 next 的時機有關。

    t.plugin('applyPluginsParallelBailResult', (next) => {
      console.log(1)
      setTimeout(() => {
        next('has args 1')
      }, 3000)
    })
    
    t.plugin('applyPluginsParallelBailResult', (next) => {
      console.log(2)
      setTimeout(() => {
        next('has args 2')
      })
    })
    
    t.plugin('applyPluginsParallelBailResult', (next) => {
      console.log(3)
      next('has args 3')
    })
    
    t.applyPluginsParallelBailResult('applyPluginsParallelBailResult', (result) => {
      console.log(result)
    })
    
    // 列印如下
    1
    2
    3
    has args 1
    
    雖然第一個 handler 的 next 函式是延遲 3s 才執行,但是註冊的順序是在最前面,所以 callback 的 result 引數值是 'has args 1'複製程式碼

    原始碼如下

    Tapable.prototype.applyPluginsParallelBailResult = function applyPluginsParallelBailResult(name) {
      var args = Array.prototype.slice.call(arguments, 1);
      var callback = args[args.length - 1];
      if(!this._plugins[name] || this._plugins[name].length === 0) return callback();
      var plugins = this._plugins[name];
      var currentPos = plugins.length;
      var currentResult;
      var done = [];
      for(var i = 0; i < plugins.length; i++) {
        args[args.length - 1] = (function(i) {
          return copyProperties(callback, function() {
            if(i >= currentPos) return; // ignore
            done.push(i);
            if(arguments.length > 0) {
              currentPos = i + 1;
              done = fastFilter.call(done, function(item) {
                return item <= i;
              });
              currentResult = Array.prototype.slice.call(arguments);
            }
            if(done.length === currentPos) {
              callback.apply(null, currentResult);
              currentPos = 0;
            }
          });
        }(i));
        plugins[i].apply(this, args);
      }
    };
    複製程式碼

    for 迴圈裡面並行的執行 handler,handler 的最後一個引數是一個匿名回撥函式,這個匿名函式必須在每個 handler 裡面手動的執行。而 callback 的執行時機就是根據 handler 的註冊順序有關。

從原始碼上來看,tapable 是提供了很多 API 來對應不同調用 handler 的場景,有同步執行,有非同步執行,還有序列非同步,並行非同步等。這些都是一些高階的技巧,不管是 express,還是 VueRouter 的原始碼,都利用這些同異步執行機制,但是可以看出程式是有邊界的。也就是約定成俗,從最後一個 applyPluginsParallel 函式來看,使用者必須呼叫匿名回撥函式,否則 tapable 怎麼知道你內部是否有非同步操作,並且非同步操作在某個時候執行完了呢。

既然知道了 0.2.8 的核心思想,那麼 2.0.0-beta 版的重構更是讓人驚豔,目前的原始碼分析正在整理,連結如下