webpack插件学习

445 阅读4分钟

webpack插件有什么用

webpack允许使用插件去访问编译时的每一个关键的环节,并暴露出核心的编译时对象供使用者操作,访问,修改,使得使用者能够在极大的自由度下控制打包编译,实现丰富的拓展功能。这就是webpack插件的用处。

webpack插件的实现

image.png

从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