webpack插件有什么用
webpack允许使用插件去访问编译时的每一个关键的环节,并暴露出核心的编译时对象供使用者操作,访问,修改,使得使用者能够在极大的自由度下控制打包编译,实现丰富的拓展功能。这就是webpack插件的用处。
webpack插件的实现
从webpack的源码中,我们可以学习到,插件的实现主要有两种。
第一种,函数形式
function MyPlugin(compiler) {
....
}
第二种,实现apply方法的对象或者类
class MyPlugin {
constructor() {}
apply(compiler) {
....
}
}
这两种方式都可以,webpack会判断其类型,如果是函数,会通过.call(compiler, compiler)的方式调用,如果不是,会尝试调用.apply方法。
通过Node API的形式使用插件
一般来说,我们会选择在webpack.config.js里面的使用,使用也很简单,将插件的实例放到plugins中就可以了,但是有时候为了更高的自由度或者特殊的需求,我们会选择使用Node API的方式使用webpack,那么我们只需要使用下面的方式就可以了。
const webpack = require('webpack'); // 访问 webpack 运行时(runtime)
const configuration = require('./webpack.config.js');
let compiler = webpack(configuration);
new webpack.ProgressPlugin().apply(compiler);
compiler.run(function (err, stats) {
// ...
});
Tapable
webpack内部使用了tapable去做到将关键环节暴露给使用者,让使用者能够以极高的自由度去控制编译的过程。
它类似EventEmitter,它会提供如下的类,实例化后,可以使用tap,tapAsync和tapPromise等方法注册钩子函数,在call调用之后,会依次调用钩子函数,并且在调用完成之后,执行回调。
webpack内部的很多环节或者说阶段,都是通过插件的方式去实现的,而这些插件实例化Hook类,然后添加到compiler或者compilation类的hooks字段上,我们可以在这些插件的Hook类上,注册钩子函数,那么在这些插件执行的时候,就会调用我们的钩子函数,方便我们做一些特殊的工作。
const {
SyncHook, // 同步钩子
SyncBailHook, // 同步熔断钩子
SyncWaterfallHook, // 同步流水钩子
SyncLoopHook, // 同步循环钩子
AsyncParallelHook, // 异步并发钩子
AsyncParallelBailHook, // 异步并发熔断钩子
AsyncSeriesHook, // 异步串行钩子
AsyncSeriesBailHook, // 异步串行熔断钩子
AsyncSeriesWaterfallHook // 异步串行流水钩子
} = require("tapable");
SyncHook的例子
SyncHook是同步的钩子,它会在调用call之后,依次调用tap注册的回调函数。
const { SyncHook } = require('tapable');
const hook = new SyncHook(['name']);
hook.tap('hello', (name) => {
console.log(`hello ${name}`);
});
hook.tap('hello again', (name) => {
console.log(`hello ${name}, again`);
});
hook.call('world');
// hello world
// hello world, again
SyncBailHook的例子
与SyncHook类似,但是如果注册的回调里返回的值不为undefined,那么后面注册的回调都不会执行。
const { SyncBailHook } = require('tapable');
const hook = new SyncBailHook(['name']);
hook.tap('hello', (name) => {
console.log(`hello ${name}`);
return false;
});
hook.tap('hello again', (name) => {
console.log(`hello ${name}, again`);
});
hook.call('world');
// hello world
SyncLoopHook的例子
SyncLoopHook注册的回调函数里,如果返回的值不是undefined,那么才会继续执行。
可以看到,hello world输出了3次之后才输出hello world again。
const { SyncLoopHook } = require('tapable');
const hook = new SyncLoopHook(['name']);
let index = 0;
hook.tap('hello', (name) => {
console.log('hello ' + name + ' ' + index);
return ++index === 3 ? undefined : true;
});
hook.tap('hello again', (name) => {
console.log('hello ' + name + ' again');
});
hook.call('world');
// hello world 0
// hello world 1
// hello world 2
// hello world again
SyncWaterfallHook的例子
注册的回调里,如果有返回值,会作为下一个注册函数的参数。
const { SyncWaterfallHook } = require('tapable');
const hook = new SyncWaterfallHook(['name']);
hook.tap('hello', (name) => {
console.log('hello ' + name);
return 'again';
});
hook.tap('hello again', (name) => {
console.log('hello ' + name);
});
hook.call('world');
// hello world
// hello again
AsyncParallelHook的例子
同步钩子只能通过tap注册回调,而异步钩子能通过tap、tapAsync、tapPromise三种方式注册回调。
异步钩子必须使用callAsync或者promise的方式调用。
只有当所有tapAsync注册的回调里面调用了cb(),tapPromise注册的回调里面返回的Promise里面resolve()了,callAsync和promise.then的回调函数才会触发。
const { AsyncParallelHook } = require('tapable');
const hook = new AsyncParallelHook(['name']);
console.time('cost');
hook.tap('greeting', function(name) {
console.log(`hello ${name}`);
});
hook.tapAsync('greeting again', function(name, cb) {
setTimeout(function() {
console.log(`hello ${name} again`);
cb();
}, 1000);
});
hook.tapPromise('greeting again again', function(name) {
return new Promise(function(resolve, reject) {
setTimeout(function() {
console.log(`hello ${name} again again`);
resolve();
}, 1000);
});
});
// hook.call('world'); // TypeError: hook.call is not a function
// hook.callAsync('world', function() {
// console.timeEnd('cost');
// });
// hello world
// hello world again ----|___同时出现
// hello world again again ----|
// cost: 1.014s
hook.promise('word').then(function() {
console.timeEnd('cost');
}).catch(function(err) {
console.log(err);
});
// hello world
// hello world again ----|___同时出现
// hello world again again ----|
// cost: 1.014s
AsyncParallelBailHook的例子
对于tap注册的回调,跟SyncBailHook一样,在返回非undefined时,会熔断,对于tapAsync和tapPromise注册的回调,cb()和resolve()调用的时候,谁先返回undefined,就会触发callAsync的回调和promse.then。
cb的第一个参数是报错信息,第二个参数才是向下传递的参数。
const { AsyncParallelBailHook } = require('tapable');
const hook = new AsyncParallelBailHook(['name']);
console.time('cost');
hook.tap('greeting', function(name) {
console.log(`hello ${name}`);
return false;
});
hook.tapAsync('greeting again', function(name, cb) {
setTimeout(function() {
console.log(`hello ${name} again`);
cb(); // cb(null, false);
}, 1000);
});
hook.tapPromise('greeting again again', function(name) {
return new Promise(function(resolve, reject) {
setTimeout(function() {
console.log(`hello ${name} again again`);
resolve(); // resolve(false);
}, 1000);
});
});
hook.callAsync('world', function(err) {
console.timeEnd('cost');
});
// hello world
// cost: 13.593ms
AsyncSeriesHook的例子
const { AsyncSeriesHook } = require('tapable');
const hook = new AsyncSeriesHook(['name']);
console.time('cost');
hook.tap('greeting', function(name) {
console.log(`hello ${name}`);
});
hook.tapAsync('greeting again', function(name, cb) {
setTimeout(function() {
console.log(`hello ${name} again`);
cb();
}, 1000);
});
hook.tapPromise('greeting again again', function(name) {
return new Promise(function(resolve, reject) {
setTimeout(function() {
console.log(`hello ${name} again again`);
resolve();
}, 1000);
});
});
hook.callAsync('world', function() {
console.timeEnd('cost');
});
// hello world
// hello world again
// hello world again again
// cost: 2.016s
AsyncSeriesBailHook的例子
这里跟异步并行不一样的是,tapAsync和tapPromise里返回非undefined后面注册的回调就不会调用了。
const { AsyncSeriesBailHook } = require('tapable');
const hook = new AsyncSeriesBailHook(['name']);
console.time('cost');
hook.tap('greeting', function(name) {
console.log(`hello ${name}`);
});
hook.tapAsync('greeting again', function(name, cb) {
setTimeout(function() {
console.log(`hello ${name} again`);
cb(null, false);
}, 1000);
});
hook.tapPromise('greeting again again', function(name) {
return new Promise(function(resolve, reject) {
setTimeout(function() {
console.log(`hello ${name} again again`);
resolve(false);
}, 1000);
});
});
hook.callAsync('world', function() {
console.timeEnd('cost');
});
// hello world
// hello world again
// cost: 1.03s
AsyncSeriesWaterfallHook的例子
tap注册的回调返回的值会给下一个注册的回调用,tapAsync注册的回调里面cb的第二个参数会给下一个注册的回调用,tapPromise注册的回调里resolve传入的值会给下一个注册的回调用。
const { AsyncSeriesWaterfallHook } = require('tapable');
const hook = new AsyncSeriesWaterfallHook(['name']);
console.time('cost');
hook.tap('greeting', function(name) {
console.log(`hello ${name}`);
return 'one';
});
hook.tapAsync('greeting again', function(name, cb) {
setTimeout(function() {
console.log(`hello ${name} again`);
cb(null, 'two');
}, 1000);
return 'two';
});
hook.tapPromise('greeting again again', function(name) {
return new Promise(function(resolve, reject) {
setTimeout(function() {
console.log(`hello ${name} again again`);
resolve('three');
}, 1000);
});
});
hook.tapPromise('greeting again again again', function(name) {
return new Promise(function(resolve, reject) {
setTimeout(function() {
console.log(`hello ${name} again again again`);
resolve();
}, 1000);
});
});
hook.callAsync('world', function() {
console.timeEnd('cost');
});
// hello world
// hello one again
// hello two again again
// hello three again again
// cost: 3.016s