深入webpack4源码(一)——插件与Tapable

包含内容:什么是Tapable、Tapable和webpack的关系、如何写一个webpack插件。

最近再开始学webpack源码,都说版本3的比4更友好,但是既然4都出了还是要看4吧?然后就直接就去clone下来边跑边看了。。然后真的是一脸懵逼。
不要把每一件事情都想得很简单,当发现这件事情很困难的时候,那就一步一步的来。

首先我们就来了解Tapable。

什么是Tapable?

Tapable也是webpack出的一个小型的库,他允许你创建勾子、为勾子挂载函数(在webpack里是挂载插件,下面再讲)、最后调用勾子。
这个模式很像发布订阅者模式。
Tapable提供一系列勾子:

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

不同勾子用法不同,看名字就能看出来:

  • 同步:SyncHook、SyncBailHook、SyncWaterfallHook、SyncLoopHook
  • 异步:AsyncParallelHook、AsyncParallelBailHook、AsyncSeriesHook、AsyncSeriesHook、AsyncSeriesBailHook、AsyncSeriesWaterfallHook

用法

  • 创建勾子
const hook = new SyncHook(["arg1", "arg2", "arg3"]);

所有类型的勾子都有一个可选的参数,参数是一个字符串数组,每个字符串代表挂载函数的参数名。

  • 为勾子挂载插件
hook.tap("name", (arg1, arg2, arg3) => {
  // TODO:
})

我们必须给每个方法传入一个名字,并且挂载的方法可以接收一系列参数。

  • 调用勾子
hook.call(arg1, arg2, arg3);

官方建议把勾子挂载class上并暴露出来。

完整的例子

const {
    SyncHook
} = require("tapable");

class Programer {
    constructor(name, address) {
        this.name = name;
        this.address = address;
        this.hooks = {
            beforeOpenComputer: new SyncHook(['name']),
            afterCloseComputer: new SyncHook(['name', 'address'])
        }
    }

    work() {
        this.hooks.beforeOpenComputer.call(this.name);
        console.log(this.name, '打开电脑');
        console.log(this.name, '开始工作');
        console.log(this.name, '关闭电脑');
        this.hooks.afterCloseComputer.call(this.name, this.address);
    }
}

let xiaoming = new Programer('小明', '回龙观');

如果我们不挂载函数直接调用xiaoming.work(),得到:

小明 打开电脑
小明 开始工作
小明 关闭电脑

我们尝试挂载函数:

xiaoming.hooks.beforeOpenComputer.tap('playPhone', name => {
    console.log(name, '玩两分钟手机');
})
xiaoming.hooks.afterCloseComputer.tap('comeback', (name, address) => {
    console.log(`${name} 吃饭`);
})
xiaoming.hooks.afterCloseComputer.tap('comeback', (name, address) => {
    console.log(`${name} 回${address}`);
})
xiaoming.work();

得到

小明 玩两分钟手机
小明 打开电脑
小明 开始工作
小明 关闭电脑
小明 吃饭
小明 回回龙观

在这里我们只展示了最简单的同步勾子SyncHook,他只接受.tap来挂载方法,并且是同步的。

SyncHook:可以挂载多个函数,挂载的函数依次执行,函数依次执行,没有返回值。

但是webpack使用的肯定不仅仅是这种勾子,所以其他类型的勾子我们也必须了解。

SyncBailHook

SyncBailHook允许返回值,并且如果有一个挂载的函数return,会终止之后的函数执行。

// this.hooks
syncBailHook: new SyncBailHook(['name']);

syncBailHook() {
    v = this.hooks.syncBailHook.call(this.name);
    console.log(v)
}

xiaoming.hooks.syncBailHook.tap('task1', name => {
    console.log('task1')
});
xiaoming.hooks.syncBailHook.tap('task2', name => {
    console.log('task2')
});
xiaoming.hooks.syncBailHook.tap('task3', name => {
    console.log('task3')
});
xiaoming.syncBailHook();

输出:

task1
task2
task3

但是我们再第二个task的地方加个return:

xiaoming.hooks.syncBailHook.tap('task2', name => {
    console.log('task2')
});

最后输出:

task1
task2
11

SyncWaterfallHook

可以挂载多个函数,函数依次执行,下一个挂载函数会利用上挂载参数的返回值作为参数。

// this.hooks
syncWaterfallHook: new SyncWaterfallHook(['name'])

syncWaterfallHook() {
    let v = this.hooks.syncWaterfallHook.call(this.name);
    console.log(v);
}

xiaoming.hooks.syncWaterfallHook.tap('task1', name => {
    console.log(name);
    return name + '1';
});
xiaoming.hooks.syncWaterfallHook.tap('task2', name => {
    console.log(name)
    return name + '2';
});
xiaoming.hooks.syncWaterfallHook.tap('task3', name => {
    console.log(name)
    return name + '3';
});
xiaoming.syncWaterfallHook();

输出

小明
小明1
小明12
小明123

SyncLoopHook

官网也是TODO,等官网写了补上

AsyncParallelHook

异步并行勾子,可以挂载多个函数,异步函数并行执行。

// this.hooks
asyncParallelHook: new AsyncParallelHook(['name']),

asyncParallelHook() {
        console.time('t');
        this.hooks.asyncParallelHook.callAsync(this.name, (err, value) => {
            console.log(value);
            console.timeEnd('t');
        });
    }

xiaoming.hooks.asyncParallelHook.tapAsync('task1', (name, callback) => {
    console.log('task1')
    setTimeout(() => {
        callback();
    }, 500)
});
xiaoming.hooks.asyncParallelHook.tapAsync('task2', (name, callback) => {
    console.log('task2')
    setTimeout(() => {
        callback();
    }, 600)
});
xiaoming.asyncParallelHook();

输出结果

task1
task2
undefined
t: 613.346ms

这里的各个异步函数并行执行,除了可以使用tapAsync/callAsync 还可以使用 tapPromise/promise这样的写法,本质没有啥区别,只是写法变了而已,可以去官网看下选择自己喜欢的方法,除此之外也可以用 tap/call这样的方式,就和同步没有区别了(tapAsync挂载的函数promise也会执行,反之同理)。

注意callback的第一个参数是err,如果返回值了,则会提前结束。

AsyncSeriesHook

串行执行异步函数

// this.hooks
asyncSeriesHook: new AsyncSeriesHook(['name'])

asyncSeriesHook() {
        console.time('t');
        this.hooks.asyncSeriesHook.callAsync(this.name, (err, value) => {
            console.log(value);
            console.timeEnd('t');
        });
    }

xiaoming.hooks.asyncSeriesHook.tapAsync('task1', (name, callback) => {
    console.log('task1')
    setTimeout(() => {
        callback();
    }, 500)
});
xiaoming.hooks.asyncSeriesHook.tapAsync('task2', (name, callback) => {
    console.log('task2')
    setTimeout(() => {
        callback();
    }, 600)
});
xiaoming.asyncSeriesHook();

输出:

task1
task2
undefined
t: 1112.619ms

AsyncParallelBailHook

没有意义,全部都并行执行了,所以没有什么提前结束,webpack源码也没有用到。

AsyncSeriesBailHook

串行执行,如果callback有返回值,则结束之后的异步操作。

asyncSeriesBailHook: new AsyncSeriesBailHook(['name']),

asyncSeriesBailHook() {
        console.time('t');
        this.hooks.asyncSeriesBailHook.callAsync(this.name, (err, value) => {
            console.log(value);
            console.timeEnd('t');
        });
    }

xiaoming.hooks.asyncSeriesBailHook.tapAsync('task1', (name, callback) => {
    console.log('task1')
    setTimeout(() => {
        callback();
    }, 500)
});
xiaoming.hooks.asyncSeriesBailHook.tapAsync('task2', (name, callback) => {
    console.log('task2')
    setTimeout(() => {
        callback(1);
    }, 500)
});
xiaoming.hooks.asyncSeriesBailHook.tapAsync('task2', (name, callback) => {
    console.log('task3')
    setTimeout(() => {
        callback();
    }, 500)
});
xiaoming.asyncSeriesBailHook();

输出:

task1
task2
1
t: 1016.677ms

AsyncSeriesWaterfallHook

串行执行,callback的值作为下一个异步函数的参数。

asyncSeriesWaterfallHook: new AsyncSeriesWaterfallHook(['name']),

asyncSeriesWaterfallHook() {
        console.time('t');
        this.hooks.asyncSeriesWaterfallHook.callAsync(this.name, (err, value) => {
            console.log(value);
            console.timeEnd('t');
        });
    }

xiaoming.hooks.asyncSeriesWaterfallHook.tapAsync('task1', (name, callback) => {
    console.log(name);
    setTimeout(() => {
        callback(null, name + '1');
    }, 500)
});
xiaoming.hooks.asyncSeriesWaterfallHook.tapAsync('task2', (name, callback) => {
    console.log(name);
    setTimeout(() => {
        callback(null, name + '2');
    }, 100)
});
xiaoming.hooks.asyncSeriesWaterfallHook.tapAsync('task3', (name, callback) => {
    console.log(name);
    setTimeout(() => {
        callback(null, name + '3');
    }, 500)
});
xiaoming.asyncSeriesWaterfallHook();

输出

小明
小明1
小明12
小明123
t: 1121.178ms

AsyncParallelWaterfallHook

同理没有任何意义,webpack也没有用到过。

总结

  • SyncHook:同步执行多个函数;
  • SyncBailHook:同步执行多个函数,有return就退出;
  • SyncWaterfallHook:同步执行多个函数,下一个的函数的参数就是上一个的返回值;
  • AsyncParallelHook:并行执行多个异步函数;
  • AsyncParallelBailHook:无意义;
  • AsyncSeriesHook:串行执行多个异步函数;
  • AsyncSeriesBailHook:串行执行多个异步函数,callback有返回值就退出;
  • AsyncSeriesWaterfallHook:串行执行多个异步函数,下一个的函数的参数就是上一个函数的callback返回值;

Tapable 和 webpack 什么关系?

webpack的整个运行机制都是建立在tapable之上的。
看了源码我们可以知道,webpack最核心的模块例如:Compiler、Compilation等,他们都继承于Tapable。


Compilation

Compiler

可想而之,要弄懂webpack,首先不可避免的先了解Tapable。

之前我们在上面的说法是勾子上挂载的函数,但是其实在webpack里叫做勾子上挂载的插件——plugin

看见plugin一定就很熟悉了,webpack的配置里有一项就是plugins:

module.exports = {
  //...
  plugins: [
    new webpack.DefinePlugin({
      // Definitions...
    })
  ]
};

webpack成功的原因之一就是灵活的插件机制,webpack一共提供了180多个勾子,你可以在任何位置,挂载任何的插件,来做一系列的操作。并且本身webpack的运行机制就是:声明一系列勾子,挂载一系列内部插件、调用插件。

但是webpack的插件和Tapable挂载的函数还是有一定的区别,直白说就是webpack做了在挂载这个地方做了一些抽象,规定了挂载的方式。

如何写一个webpack插件?

一个插件由以下构成:

  • 一个具名 JavaScript 函数。
  • 在它的原型上定义 apply 方法。
  • 指定一个触及到 webpack 本身的 事件钩子。
  • 操作 webpack 内部的实例特定数据。
  • 在实现功能后调用 webpack 提供的 callback。
// 一个 JavaScript class
class MyExampleWebpackPlugin {
  // 将 `apply` 定义为其原型方法,此方法以 compiler 作为参数
  apply(compiler) {
    // 指定要附加到的事件钩子函数
    compiler.hooks.emit.tapAsync(
      'MyExampleWebpackPlugin',
      (compilation, callback) => {
        console.log('This is an example plugin!');
        console.log('Here’s the `compilation` object which represents a single build of assets:', compilation);

        // 使用 webpack 提供的 plugin API 操作构建结果
        compilation.addModule(/* ... */);

        callback();
      }
    );
  }
}

我们必须定义一个apply方法,此方法都以webpack的核心模块compiler实例作为参数,就像我们之前的示例一样,compiler一样有一个暴露出来的对象hooks,hooks包含着很多不同类型不同运行 阶段的勾子,然后这个时候我们就可以像之前例子那样对对应的勾子编写实际操作。

说真的,写一个插件的成本真的挺高的,你必须看过源码,对内部运行阶段了如指掌才行。具体的一些常用勾子这里有文档:插件api。
更多的勾子可以参考:Webpack 内部插件与钩子关系

到这里你才能真正的开始去看源码。

参考文献

  • 插件api
  • 【webpack进阶】可视化展示webpack内部插件与钩子关系
  • Tapable
  • webpack源码

你可能感兴趣的:(深入webpack4源码(一)——插件与Tapable)