上一篇我們經(jīng)歷了一次頭腦爆炸,一口氣看完了從webpack-cli到babel-loader的全流程宽闲。
這一篇可以放松一下了悲雳,
來(lái)看看hooks中到底包含了什么秘密。
1. hooks回顧
我們知道在webpack Compiler.js中第536行陈哑,
this.compile
中調(diào)用了hooks.make
,
compile(callback) {
const params = this.newCompilationParams();
this.hooks.beforeCompile.callAsync(params, err => {
if (err) return callback(err);
this.hooks.compile.call(params);
const compilation = this.newCompilation(params);
this.hooks.make.callAsync(compilation, err => {
if (err) return callback(err);
compilation.finish();
compilation.seal(err => {
if (err) return callback(err);
this.hooks.afterCompile.callAsync(compilation, err => {
if (err) return callback(err);
return callback(null, compilation);
});
});
});
});
}
結(jié)果 this.hooks.make.callAsync(compilation, err => {
居然會(huì)跳轉(zhuǎn)到伸眶,SingleEntryPlugin.js 文件中芥颈。
compiler.hooks.make.tapAsync(
"SingleEntryPlugin",
(compilation, callback) => {
const { entry, name, context } = this;
const dep = SingleEntryPlugin.createDependency(entry, name);
compilation.addEntry(context, dep, name, callback);
}
);
這其中發(fā)生了什么?
為了理解這一點(diǎn)赚抡,我們還得從webpack plugin說(shuō)起。
2. webpack插件的編寫(xiě)方式
我認(rèn)為從用例中學(xué)習(xí)代碼庫(kù)纠屋,會(huì)把事情變簡(jiǎn)單涂臣,
因?yàn)榇a的設(shè)計(jì)目的,就是為它的使用場(chǎng)景服務(wù)的售担。
所以赁遗,一開(kāi)始我們不宜直接研究hooks是如何實(shí)現(xiàn)的,
而是看看hooks的設(shè)計(jì)者們族铆,期望它被如何使用岩四。
hooks最常見(jiàn)的使用場(chǎng)景,就是當(dāng)我們?cè)诮owebpack編寫(xiě)插件的時(shí)候哥攘,
插件中會(huì)實(shí)現(xiàn)compiler
和compilation
對(duì)象的多個(gè)hooks剖煌,
下面我們來(lái)創(chuàng)建一個(gè)webpack 插件材鹦。
2.1 test-plugin.js
在我們的debug-webpack工程中,我們?cè)诟夸浿行陆ㄒ粋€(gè)test-plugin.js文件耕姊。
module.exports = class Plugin {
apply(compiler) {
compiler.hooks.make.tapAsync('TestPlugin', (compilation, callback) => {
compilation.hooks.buildModule.tap('TestPlugin', module => {
console.log('module.resource', module.resource);
console.log('module.loaders', module.loaders);
console.time('TestPlugin');
});
compilation.hooks.succeedModule.tap('TestPlugin', module => {
console.timeEnd('TestPlugin');
});
callback();
});
}
};
它導(dǎo)出了一個(gè)類桶唐,這個(gè)類必須實(shí)現(xiàn)apply
方法。
其中茉兰,apply方法的形參是compiler
尤泽,
就是webpack-cli中調(diào)用的compiler.run
的那個(gè)compiler
。
至于更細(xì)節(jié)的問(wèn)題规脸,我們以后可以再慢慢看坯约。
(1)compiler.hooks.make.tapAsync
compiler.hooks.make.tapAsync
實(shí)現(xiàn)了compiler.hooks.make
,
如果我們?cè)黾恿诉@個(gè)實(shí)現(xiàn)莫鸭,
webpack Compiler.js 第536行 build
調(diào)用 this.hooks.make.callAsync(compilation, err => {
會(huì)額外觸發(fā)我們這里的實(shí)現(xiàn)闹丐。
因?yàn)樗钱惒降模晕覀冏詈笠{(diào)用 callback
來(lái)完成調(diào)用黔龟。
compiler.hooks.make.tapAsync('TestPlugin', (compilation, callback) => {
...
callback();
});
(2)compilation.hooks.buildModule.tap
compilation.hooks.buildModule.tap
實(shí)現(xiàn)了compilation.hooks.buildModule
妇智,
它會(huì)在webpack Compilation.js 第617行,buildModule
中氏身,
執(zhí)行this.hooks.buildModule.call(module);
時(shí)被調(diào)用巍棱。
buildModule(module, optional, origin, dependencies, thisCallback) {
...
this.hooks.buildModule.call(module);
module.build(
...
}
因此,通過(guò)將compiler.hooks
和compilation.hooks
的調(diào)用和實(shí)現(xiàn)分離蛋欣,
相當(dāng)于在webpack執(zhí)行過(guò)程中航徙,添加了多個(gè)切面(面向切面編程AOP)。
在這些切面中陷虎,webpack插件可以做自己想做的任何事情到踏。
以上test-plugin.js插件,我們只是統(tǒng)計(jì)了一下尚猿,
從compilation.buildModule
到compilation.succeedModule
所經(jīng)歷的時(shí)間窝稿。
注:compilation.hooks.succeedModule
在Compilation.js 第652行調(diào)用。
this.hooks.succeedModule.call(module);
2.2 在webpack.config.js中使用插件
const path = require('path');
const TestPlugin = require('./test-plugin');
module.exports = {
entry: {
index: path.resolve(__dirname, 'src/index.js'),
},
output: {
path: path.resolve(__dirname, 'dist/'),
},
module: {
rules: [
{ test: /\.js$/, use: { loader: 'babel-loader', query: { presets: ['@babel/preset-env'] } } },
]
},
plugins:[
new TestPlugin(),
],
};
我們?cè)趙ebpack.config.js中引入了test-plugin.js凿掂,
然后在導(dǎo)出對(duì)象中增加了plugins
屬性伴榔。
這樣我們的webpack插件就編寫(xiě)完了。
2.3 查看插件的調(diào)用效果
直接調(diào)用npm run build
庄萎,
$ npm run build
module.resource ~/Test/debug-webpack/src/index.js
module.loaders [ { options: { presets: [Array] },
ident: 'ref--4',
loader: '~/Test/debug-webpack/node_modules/_babel-loader@8.0.4@babel-loader/lib/index.js' } ]
TestPlugin: 213.301ms
我們統(tǒng)計(jì)出來(lái)踪少,build ./src/index.js 源文件,
從compilation.hooks.buildModule
到compilation.hooks.succeedModule
糠涛,
總共花費(fèi)了 213.301ms
援奢。
3. Tapable
上文中我們通過(guò)hooks的用例,了解了它的使用方式忍捡,
它為Compiler和Compilation兩個(gè)類實(shí)現(xiàn)了多個(gè)切面集漾,
下面我們來(lái)看一下原理切黔。
我們打開(kāi)Compiler類的源碼,位于Compiler.js 第40行帆竹,
它是Tapable
的子類绕娘,它是從tapable模塊中導(dǎo)出的,
tapable是一個(gè)獨(dú)立的代碼庫(kù)(v1.1.0)栽连。
直接閱讀tapable源碼會(huì)發(fā)現(xiàn)非常難懂险领,
HookCodeFactory.js 中使用了大量的元編程手段new Function
。
fn = new Function(
this.args(),
'"use strict";\n' +
this.header() +
this.content({
onError: err => `throw ${err};\n`,
onResult: result => `return ${result};\n`,
onDone: () => "",
rethrowIfPossible: true
})
);
我認(rèn)為在這種情況下秒紧,要想更好的理解它绢陌,最好的辦法是從動(dòng)機(jī)入手,
在tapable倉(cāng)庫(kù)README.md中熔恢,
我們看到脐湾,tapable實(shí)現(xiàn)了interception(攔截器)。
const { SyncHook } = require('tapable');
const hook = new SyncHook(['x', 'y']);
hook.intercept({
register(...args) {
console.log('2. in: register', args);
},
call(...args) {
console.log('4. in: call', args);
},
});
console.log('1. start: register');
hook.tap('some-message', (...args) => {
console.log('5. in: tap', args);
});
console.log('3. start: call');
hook.call(1, 2, 3);
日志信息如下叙淌,
1. start: register
2. in: register [ { type: 'sync', fn: [Function], name: 'some-message' } ]
3. start: call
4. in: call [ 1, 2 ]
5. in: tap [ 1, 2 ]
注:
hooks在構(gòu)造時(shí)設(shè)置了兩個(gè)參數(shù)(x
和y
)秤掌,
但是在調(diào)用時(shí)用了三個(gè)參數(shù)(1
,2
和3
)鹰霍,結(jié)果第三個(gè)參數(shù)丟失了闻鉴。
我們看到,每一個(gè)hooks在被調(diào)用的時(shí)候它都可以攔截到茂洒,被添加新的實(shí)現(xiàn)時(shí)也是如此孟岛。
我們知道ES6 proxy也可以用來(lái)實(shí)現(xiàn)攔截功能,
但是IE卻一直是不支持的督勺。
因此渠羞,tapable中采用了兼容性的做法,對(duì)實(shí)現(xiàn)hooks的代碼進(jìn)行了動(dòng)態(tài)修改智哀,
在其前后增加了攔截器次询,最后再通過(guò)new Function
生成一個(gè)函數(shù)。
具體的源碼解析瓷叫,我們以后慢慢補(bǔ)充渗蟹。