文章首发于我的博客 https://github.com/mcuking/bl...

接着上文 Webpack 源码剖析(1)—— Webpack 启动过程剖析 咱们接下来持续剖析 webpack 的构建流程。

上文结尾处咱们提到了 webpack-cli 最终还是调用了 webpack 提供的 webpack 函数,取得了 compiler 实例对象。那么咱们就从新回到 webpack 包查看下这个 webpack 函数,webpack 函数所在文件是 node_module\webpack\lib\webpack.js 。上面是其中的要害代码:

const Compiler = require("./Compiler");...const webpack = (options, callback) => {    ...    let compiler;    if (Array.isArray(options)) {        compiler = new MultiCompiler(            Array.from(options).map(options => webpack(options))        );    } else if (typeof options === "object") {        options = new WebpackOptionsDefaulter().process(options);        compiler = new Compiler(options.context);        compiler.options = options;        ...        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);                }            }        }        compiler.hooks.environment.call();        compiler.hooks.afterEnvironment.call();        compiler.options = new WebpackOptionsApply().process(options, compiler);    } else {        throw new Error("Invalid argument: options");    }    if (callback) {        ...        compiler.run(callback);    }    return compiler;}

有下面的代码咱们能够看到 webpack 函数是通过引入了内部定义好的 Compiler 类,并基于接管到的 options 初始化了一个实例对象(如果 options 是数组,则遍历数组中每个 option,别离初始化 compiler 实例对象),最初调用了 compiler 实例上的 run 办法(如果是 watch 模式则调用 watch 办法)。

不过在调用 run 办法之前,还有一些逻辑是对 options 的 plugins 属性做了一些解决以及调用 compiler 下面的 hooks 的一些办法。为了搞清楚这里的原理,咱们须要认真理解下 Compiler 这个类的定义,该类在 node_module\webpack\lib\Compiler 文件中。要害代码如下:

const {    Tapable,    SyncHook,    SyncBailHook,    AsyncParallelHook,    AsyncSeriesHook} = require("tapable");const Compilation = require("./Compilation");class Compiler extends Tapable {    constructor(context) {        super();        this.hooks = {            shouldEmit: new SyncBailHook(["compilation"]),            done: new AsyncSeriesHook(["stats"]),            ...        },        ...    }    watch(watchOptions, handler) {    }    run(callback) {    }    ...    emitAssets(compilation, callback) {    }    ...    createCompilation() {        return new Compilation(this);    }    newCompilation(params) {    }        ...    compile(callback) {    }}

到这里咱们理解到 Compiler 类继承了 Tapable 类,而 Tapable 类又是从 webpack 开源的 tapable 包中引入的,那么接下来就须要弄清 tapable 这个包的作用了。

对于 tapable 的外部源码咱们就不去剖析了,而是采纳另一种思路,通过查问 tapable 仓库的文档和相干材料,参考 webpack 中应用 Tapable 的形式,用代码实现一个相似的 demo。

Tapable 是什么

Tapable 是一个相似 NodeJS 的 EventEmitter 的库,次要通过钩子函数的公布与订阅来实现 webpack 的插件零碎。

Tapable 的根本应用

Tapable 裸露进去的都是类办法,能够通过 new 一个类办法来取得咱们须要的钩子。

那么咱们看下 Tapable 裸露进去的 Hook(钩子)类都有哪些,总共 9 种:

const {    SyncHook,    SyncBailHook,    SyncWaterfallHook,    SyncLoopHook,    AsyncParallelHook,    AsyncParallelBailHook,    AsyncSeriesHook,    AsyncSeriesBailHook,    AsyncSeriesWaterfallHook,} = require("tapable")

不难发现,其中有很多公共的局部,其实这九种钩子都是继承了上面列表中的根底钩子类:

typefunction
Hook所有钩子的后缀
Waterfall同步办法,然而会传值给下一个办法
Bail熔断:当函数有任何返回值,都会在以后执行函数进行
Loop监听函数返回,返回 true 持续循环,返回 undefined 则完结循环
Sync同步
Async异步
AsyncSeries异步串行
AsyncParallel异步并行

具体是通过钩子的绑定和执行来应用的,如下图:

AsyncSync
绑定:tapAsync/tapPromise/tap绑定:tap
执行:callAsync/promise执行:call

上面是 hook 应用示例代码:

const hook1 = new SyncHook(["arg1", "arg2"])// 绑定事件hook1.tap('hook1', (arg1, arg2) => console.log(arg1, arg2));// 执行绑定的事件hook1.call(1, 2);

模仿 Webpack 应用 Tapable 形式

源码请参考 https://github.com/mcuking/bl...

咱们首先依照源码的形式实现一个简略的 Compiler 类,并设置两个钩子 compile(同步钩子)和 emit(异步串行钩子)。

const { SyncHook, AsyncSeriesHook } = require('tapable');class Compiler {  constructor() {    super();    this.hooks = {      compile: new SyncHook(),      emit: new AsyncSeriesHook()    };  }  run() {    this.compile();    this.emit();  }  compile() {    this.hooks.compile.call();  }  emit() {    this.hooks.emit.callAsync(() => {});  }}module.exports = Compiler;

而后咱们在调用 Compiler 实例对象的 run 办法时,执行刚刚两个钩子 compile 和 emit。接下来咱们实现一个插件 myPlugin,也是依照类的模式来实现的。

class MyPlugin {  constructor() {}  apply(compiler) {    compiler.hooks.compile.tap('OfflinePackagePlugin', () => {      console.log('compiling...');    });    compiler.hooks.emit.tapAsync('OfflinePackagePlugin', callback => {      console.log('start generating offline package...');      setTimeout(() => {        console.log('generate offline package successfully');        callback();      }, 4000);    });  }}module.exports = MyPlugin;

插件中 apply 办法接管了 Compiler 实例对象,并调用了实例上的两个 hook (钩子)的绑定办法。

而后再 index.js 文件中将 Compiler 和 MyPlugin 联合起来,如下:

/** * 模仿 webpack 应用 tapable 形式, * 用来演示 webpack 外部插件运行机制 */const Compiler = require('./Compiler');const MyPlugin = require('./myPlugin');const myPlugin = new MyPlugin();const options = {  plugins: [myPlugin]};const compiler = new Compiler();for (const plugin of options.plugins) {  if (typeof plugin === 'function') {    plugin.call(compiler, compiler);  } else {    plugin.apply(compiler);  }}compiler.run();

即初始化了一个 Compiler 实例对象,而后初始化了 options 外面的插件(传入Compiler 的实例对象),其实就是将插件外面的要执行的业务逻辑绑定到 Compiler 实例的 hook (钩子)上,最初执行 Compiler 实例对象的 run 办法,触发相应的 hook (钩子),从而触发绑定到 hook (钩子)上的办法的执行,实质上就是公布订阅模式。

到这里咱们就曾经把握了 webpack 是如何利用 tapable 来实现整个插件机制的,下篇文章咱们将真正开始对 webpack 的构建流程进行解析。