webpack plugin 探索 (内含详细 tapable 讲解)

990 阅读8分钟

插件是 webpack 的 tapable 功能。webpack 自身也是构建于你在 webpack 配置中用到的相同的插件系统之上!插件目的在于解决 loader 无法实现的其他事。

上面是官网给出的解释,听起来十分的酷炫。那么要怎么去使用并写出心仪的plugin呢?随之在官网找到了下面的demo。

const pluginName = 'ConsoleLogOnBuildWebpackPlugin';

class ConsoleLogOnBuildWebpackPlugin {
  apply(compiler) {
    compiler.hooks.run.tap(pluginName, compilation => {
      console.log('webpack 构建过程开始!');
    });
  }
}

module.exports = ConsoleLogOnBuildWebpackPlugin;

看完后陷入了大大的疑惑

  • apply 什么时候调用的?
  • compiler 是啥?
  • 传入的 compilation 是什么

官网大大给出了简单的解释:webpack 插件是一个具有 apply方法的 JavaScript 对象。apply 方法会被 webpack compiler 调用,并且在整个编译生命周期都可以访问 compiler 对象。

为了更加深入的理解这些问题,一路溯源,那么首先看看 apply 的执行时机

apply什么时候调用的?

一路从webpack bin/webpack.js -> webpack-cli bin/cli.js -> webpack lib/webpack.js ->

然后发现了下面的代码。【源码中如何找到目的文件的? 请自行了解下package.json中的main 和 bin】

if (options.plugins && Array.isArray(options.plugins)) {
  for (const plugin of options.plugins) {
    if (typeof plugin === "function") {
      plugin.call(compiler, compiler);
    } else {
      plugin.apply(compiler);
    }
  }
}

options 就是我们传入的 webpack 配置,一般 plugin 是个 class,在自定义的 plugin 中添加 apply 这个方法,这样是因为webpack会去执行 apply 这个方法,往里面传入 compiler 这个对象。

这样我们遇到了第二个问题 compiler 是啥?

compiler 是啥?

Compiler 继承于 Tapable 并在hooks属性中包含了26个生命周期,并且这些生命周期都是 Tapable 中函数的实例。

options.context = process.cwd();        
compiler = new Compiler(options.context);

bin/Compiler.js

const {
    Tapable,
    ...
    AsyncSeriesHook
} = require("tapable");

class Compiler extends Tapable {
    constructor(context) {
        super();
        this.hooks = {
            ...
            /** @type {AsyncSeriesHook<Compiler>} */
            run: new AsyncSeriesHook(["compiler"]),
            ...
}

为了更好的理解这些生命周期的执行方式,一起来了解下webpack的核心 tapable

tapable 干啥的?

打开 tapable ,映入眼帘的就是一堆的方法。。。

exports.__esModule = true;
exports.SyncHook = require("./SyncHook");
exports.SyncBailHook = require("./SyncBailHook");
exports.SyncWaterfallHook = require("./SyncWaterfallHook");
exports.SyncLoopHook = require("./SyncLoopHook");
exports.AsyncParallelHook = require("./AsyncParallelHook");
exports.AsyncParallelBailHook = require("./AsyncParallelBailHook");
exports.AsyncSeriesHook = require("./AsyncSeriesHook");
exports.AsyncSeriesWaterfallHook = require("./AsyncSeriesWaterfallHook");

首先我们简单的使用下,下面拿 SyncHook 举例

tapble是怎么使用的?

所有的 hook 构造函数接受一个参数,这个参数是个数组。

const hook = new SyncHook(["arg1", "arg2", "arg3"]);

官方的建议是将钩子放在类实例的 hooks 属性上

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

class Car {
    constructor() {
        this.hooks = {
            accelerate: new SyncHook(["newSpeed"]),
            brake: new SyncHook()
        };
    }
}

const myCar = new Car();
myCar.hooks.brake.tap("WarningLampPlugin", () => console.log("减速慢行"));
myCar.hooks.accelerate.tap("LoggerPlugin", newSpeed => console.log(`速度到 ${newSpeed}`))

myCar.hooks.brake.call();
myCar.hooks.accelerate.call("100km/h");

// 减速慢行
// 速度到 100km/h

tapable 的钩子本质基于发布/订阅模式,通过 taptapAsynctapPromise 等方法注册事件,通过 callcallAsyncpromise 等方法派发事件

深入理解tapble

tapble中的这些方法都是有一定规律的:

  • 若钩子带有 Series 字样,注册的事件函数会串行执行。Series 自带串行的意思。
  • 若钩子带有 parallel 字样,注册的事件函数会并行执行。parallel 自带并行的意思。
  • 若钩子中带有 Bail 字样,当任何被执行的函数返回任何内容时,将停止执行剩余的钩子,如果返回undefined 将继续执行。B``ail 保险的意思。
  • 若钩子中带有 Waterfall 字样,它会将每个函数的返回值传递给下一个函数。Waterfall 瀑布的意思。
  • 若钩子中带有 Loop 字样,表示函数将在某种情况下循环执行,Loop 循环的意思。

理解同步方法

tapable 中包含四个同步钩子

同步的钩子支持使用 tap 注册事件,支持使用 call 派发事件。

exports.SyncHook = require("./SyncHook");
exports.SyncBailHook = require("./SyncBailHook");
exports.SyncWaterfallHook = require("./SyncWaterfallHook");
exports.SyncLoopHook = require("./SyncLoopHook");
SyncBailHook 详解

当任何被执行的函数返回任何内容时,将停止执行剩余的钩子,如果返回 undefined 将继续执行。

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

class Car {
    constructor() {
        this.hooks = {
            accelerate: new SyncBailHook(),
        };
    }
}

const myCar = new Car();
myCar.hooks.accelerate.tap("100km/h", () => {
    console.log("100km/h");
    return "有返回值";
});
myCar.hooks.accelerate.tap("110km/h", () => {
    console.log("110km/h");
    return 
})

myCar.hooks.accelerate.call();
// 100km/h
SyncWaterfallHook 详解

SyncWaterfallHook 钩子将在函数运行产生返回值时将返回值传递给下一个函数。

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

class Car {
    constructor() {
        this.hooks = {
            accelerate: new SyncWaterfallHook(["speed"]),
        };
    }
}

const myCar = new Car();
myCar.hooks.accelerate.tap("100km/h", (speed) => {
    console.log(speed);
    return "加速到110km/h";
});
myCar.hooks.accelerate.tap("110km/h", (speed) => {
    console.log(speed);
    return 
})

myCar.hooks.accelerate.call("100km/h");

// 100km/h
// 加速到110km/h
SyncLoopHook 详解

SyncLoopHook 钩子让函数具备重复执行的可能性,如果某个函数的返回值不为 undefined,那么整个任务队列将回到开始重新执行,直到所有的函数都返回 undefined才会停止。

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

class Car {
    constructor() {
        this.hooks = {
            accelerate: new SyncLoopHook(),
        };
    }
}

const myCar = new Car();
myCar.hooks.accelerate.tap("100km/h", () => {
    console.log("100km/h");
    return Math.random() > 0.5 ? "" : undefined
});
myCar.hooks.accelerate.tap("110km/h", () => {
    console.log("110km/h");
    return Math.random() > 0.5 ? "" : undefined
})

myCar.hooks.accelerate.call();
const {SyncLoopHook} = require("tapable");

class Car {
    constructor() {
        this.hooks = {
            accelerate: new SyncLoopHook(),
        };
    }
}

const myCar = new Car();
myCar.hooks.accelerate.tap("100km/h", () => {
    console.log("100km/h");
    return Math.random() > 0.5 ? "" : undefined
});
myCar.hooks.accelerate.tap("110km/h", () => {
    console.log("110km/h");
    return Math.random() > 0.5 ? "" : undefined
})

myCar.hooks.accelerate.call();
// 100km/h
// 110km/h
// 100km/h
// 100km/h
// 100km/h
// 110km/h
// 100km/h
// 110km/h

理解异步方法

异步的钩子支持使用 tapAsynctapPromie 注册事件,使用 callAsyncpromise 派发事件。

callAsync 方法的最后一个参数是一个回调函数,这个回调函数会在整个异步任务执行完成后再执行。

所有的钩子函数在运行时,都会接受一个 done 函数作为参数,只有这些 done 函数都被调用后,整个异步任务才会被认为执行完成,这时才会执行 callAsync 的回调函数。

exports.AsyncParallelHook = require("./AsyncParallelHook");
exports.AsyncParallelBailHook = require("./AsyncParallelBailHook");
exports.AsyncSeriesHook = require("./AsyncSeriesHook");
exports.AsyncSeriesWaterfallHook = require("./AsyncSeriesWaterfallHook");
AsyncParallelHook 理解

为了表示出串行和并行的区别这里使用了 setTimeout 作为辅助。

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

class Car {
    constructor() {
        this.hooks = {
            accelerate: new AsyncParallelHook(["speed"]),
        };
    }
}

const myCar = new Car();
myCar.hooks.accelerate.tapAsync("100km/h", (speed, done) => {
    console.log(1, speed);
    setTimeout(() => {
        console.log(1, "done");
        done()
    }, 2000)

});
myCar.hooks.accelerate.tapAsync("110km/h", (speed, done) => {
    console.log(2, speed);
    setTimeout(() => {
        console.log(2, "done");
        done()
    }, 2000)

})
myCar.hooks.accelerate.tapAsync("120km/h", (speed, done) => {
    console.log(3, speed);
    setTimeout(() => {
        console.log(3, "done");
        done()
    }, 2000)

})

myCar.hooks.accelerate.callAsync("110km/h", () => {
    console.log("加速完成");
});
// 1 '110km/h'
// 2 '110km/h'
// 3 '110km/h'
// 1 'done'
// 2 'done'
// 3 'done'
// 加速完成
AsyncParallelBailHook 理解

结合上面对ParallelBail 的讲解,可以先大概的猜一下这个钩子的功能是啥。

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

class Car {
    constructor() {
        this.hooks = {
            accelerate: new AsyncParallelBailHook(["speed"]),
        };
    }
}

const myCar = new Car();
myCar.hooks.accelerate.tapAsync("100km/h", (speed, done) => {
    console.log(1, speed);
    setTimeout(() => {
        console.log(1, "done");
        done()
    }, 2000)

});
myCar.hooks.accelerate.tapAsync("110km/h", (speed, done) => {
    console.log(2, speed);
    setTimeout(() => {
        console.log(2, "done");
        done()
    }, 2000)

})
myCar.hooks.accelerate.tapAsync("120km/h", (speed, done) => {
    console.log(3, speed);
    setTimeout(() => {
        console.log(3, "done");
        done()
    }, 2000)

})

myCar.hooks.accelerate.callAsync("110km/h", () => {
    console.log("加速完成");
});

// 1 '110km/h'
// 2 '110km/h'
// 3 '110km/h'
// 1 'done'
// 2 'done'
// 加速完成
// 3 'done'
AsyncSeriesHook 理解

到这里应该理解越来越容易了, done 中传入参数会达到 Bail 的效果,会暂停现在的传递,直接调用 callAsync 中的回调函数。

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

class Car {
    constructor() {
        this.hooks = {
            accelerate: new AsyncSeriesHook(["speed"]),
        };
    }
}

const myCar = new Car();
myCar.hooks.accelerate.tapAsync("100km/h", (speed, done) => {
    console.log(1, speed);
    setTimeout(() => {
        console.log(1, "done");
        done()
    }, 2000)

});
myCar.hooks.accelerate.tapAsync("110km/h", (speed, done) => {
    console.log(2, speed);
    setTimeout(() => {
        console.log(2, "done");
        done()
    }, 2000)

})
myCar.hooks.accelerate.tapAsync("120km/h", (speed, done) => {
    console.log(3, speed);
    setTimeout(() => {
        console.log(3, "done");
        done()
    }, 2000)

})

myCar.hooks.accelerate.callAsync("110km/h", () => {
    console.log("加速完成");
});

// 1 '110km/h'
// 1 'done'
// 2 '110km/h'
// 2 'done'
// 3 '110km/h'
// 3 'done'
// 加速完成
AsyncSeriesWaterfallHook 理解

串行加上参数的传递

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

class Car {
    constructor() {
        this.hooks = {
            accelerate: new AsyncSeriesWaterfallHook(["speed"]),
        };
    }
}

const myCar = new Car();
myCar.hooks.accelerate.tapAsync("100km/h", (speed, done) => {
    console.log(1, speed);
    setTimeout(() => {
        console.log(1, "done");
        done(null, "110km/h")
    }, 2000)

});
myCar.hooks.accelerate.tapAsync("110km/h", (speed, done) => {
    console.log(2, speed);
    setTimeout(() => {
        console.log(2, "done");
        done(null, "120km/h")
    }, 2000)

})
myCar.hooks.accelerate.tapAsync("120km/h", (speed, done) => {
    console.log(3, speed);
    setTimeout(() => {
        console.log(3, "done");
        done()
    }, 2000)

})

myCar.hooks.accelerate.callAsync("100km/h", () => {
    console.log("加速完成");
});

// 1 '10km/h'
// 1 'done'
// 2 '110km/h'
// 2 'done'
// 3 '120km/h'
// 3 'done'
// 加速完成

tapPromise 和 promise 的配合相较于上面 只是需要返回的是个Promise。可以仿照下面的这个例子自行理解下

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

class Car {
    constructor() {
        this.hooks = {
            accelerate: new AsyncSeriesWaterfallHook(["speed"]),
        };
    }
}

const myCar = new Car();
myCar.hooks.accelerate.tapPromise("100km/h", (speed) => {
    return new Promise((res) => {
        console.log(1, speed);
        setTimeout(() => {
            console.log(1, "done");
            res("110km/h")
        }, 2000)
      })

});
myCar.hooks.accelerate.tapPromise("110km/h", (speed) => {
    return new Promise((res) => {
        console.log(2, speed);
        setTimeout(() => {
            console.log(2, "done");
            res("120km/h")
        }, 2000)
      })

})
myCar.hooks.accelerate.tapPromise("120km/h", (speed) => {
    return new Promise((res) => {
        console.log(3, speed);
        setTimeout(() => {
            console.log(3, "done");
            res()
        }, 2000)
    })
})

myCar.hooks.accelerate.promise("100km/h").then(() => {
    console.log("加速完成");
});

到此再回头看 webpack 中的那些生命周期钩子就一目了然了。

再看Compiler

this.hooks = {
            ...
            /** @type {SyncBailHook<Compilation>} */
            shouldEmit: new SyncBailHook(["compilation"]),
            /** @type {AsyncSeriesHook<Stats>} */
            done: new AsyncSeriesHook(["stats"]),
            /** @type {AsyncSeriesHook<>} */
            additionalPass: new AsyncSeriesHook([]),
            /** @type {AsyncSeriesHook<Compiler>} */
            beforeRun: new AsyncSeriesHook(["compiler"]),
            /** @type {AsyncSeriesHook<Compiler>} */
            run: new AsyncSeriesHook(["compiler"]),
            /** @type {AsyncSeriesHook<Compilation>} */
            emit: new AsyncSeriesHook(["compilation"]),
            /** @type {AsyncSeriesHook<string, Buffer>} */
           ...
    };

结合【Compiler】,现在了解到 webpack 执行的大概过程,我们写的 plugin 会在 webpack 执行的某个生命周期阶段被call promise callAsync 调用。

但是要想自己自定义的组件功能更加强大, 还需要了解传入的参数 compilation

Compilation

Compilation 对象代表了一次资源版本构建。当运行 webpack 开发环境中间件时,每当检 测到一个文件变化,就会创建一个新的 compilation,从而生成一组新的编译资源。一个 compilation 对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息。compilation 对象也提供了很多关键步骤的回调,以供插件做自定义处理时选择使用。

Compilation 也继承于 Tapable ,并且也具有自己的生命周期方法,它的生命周期方法更多,但是理解的方式和Compiler相似,因为都是基于 Tapble 中的钩子。【Compilation

自定义组件

在 编译(compilation)完成之后打开浏览器,传入响应的url。

const pluginName = 'OpenBrowserWebpackPlugin';
var child_process = require("child_process");
class OpenBrowserWebpackPlugin {
    constructor(options) {
        this.options = options    
    }
    apply(compiler) {
        compiler.hooks.done.tap(pluginName, compilation => {
            let cmd = "";
            switch (process.platform) {
                case 'wind32':
                    cmd = 'start';
                    break;

                case 'linux':
                    cmd = 'xdg-open';
                    break;

                case 'darwin':
                    cmd = 'open';
                    break;
            }
            console.log('打开浏览器!');
            child_process.exec(cmd + ' ' + this.options.url);
        });
    }
}

module.exports = OpenBrowserWebpackPlugin;

其他更加复杂的插件开发,一起探索吧!!!

如果觉得对你有帮助,请留下一个赞吧!!