插件是 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
的钩子本质基于发布/订阅模式,通过tap
,tapAsync
,tapPromise
等方法注册事件,通过call
,callAsync
,promise
等方法派发事件
深入理解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
理解异步方法
异步的钩子支持使用 tapAsync
和 tapPromie
注册事件,使用 callAsync
和 promise
派发事件。
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 理解
结合上面对Parallel
和 Bail
的讲解,可以先大概的猜一下这个钩子的功能是啥。
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;
其他更加复杂的插件开发,一起探索吧!!!
如果觉得对你有帮助,请留下一个赞吧!!