深入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');
    return 11;
});

最后输出:

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 内部插件与钩子关系

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

参考文献

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342