实现一个简易的Webpack-plugin
# 前言
plugin在webpack中也是非常重要的一环,他可以提供给了用户在webpack执行的任意周期的钩子,让用户可以在任意地方去穿插自己的想法,从而改变构建结果。下面就让我们一起来学习一下一个plugin吧
# 基本结构
一个最基本的 plugin 需要包含这些部分:
- 一个 JavaScript 类
- 一个 apply 方法,apply 方法在 webpack 装载这个插件的时候被调用,并且会传入 compiler 对象。
- 使用不同的 hooks 来指定自己需要发生的处理行为
- 在异步调用时最后需要调用 webpack 提供给我们的 callback 或者通过 Promise 的方式(后续异步编译部分会详细说)
/**
* hookName 需要订阅的hook的名称
* PluginName 你的插件名称,字符串格式
*/
class HelloPlugin{
apply(compiler){
compiler.hooks[hookName].tap(PluginName,(params)=>{
/** do some thing */
})
}
}
module.exports = HelloPlugin
2
3
4
5
6
7
8
9
10
11
12
13
# compile和compilation
在学习plugin之前首先要来学习一下 compile和compilation,这两个对象到底是什么呢? 用官方的话来说
- Compiler模块是 webpack 的主要引擎,它通过CLI或者Node API传递的所有选项创建出一个 compilation 实例。 它扩展(extends)自Tapable类,用来注册和调用插件。 大多数面向用户的插件会首先在Compiler上注册。
- Compilation 模块会被 Compiler 用来创建新的 compilation 对象(或新的 build 对象)。 compilation 实例能够访问所有的模块和它们的依赖(大部分是循环依赖)。 它会对应用程序的依赖图中所有模块, 进行字面上的编译(literal compilation)。 在编译阶段,模块会被加载(load)、封存(seal)、优化(optimize)、 分块(chunk)、哈希(hash)和重新创建(restore)。
通俗一点将就是compiler是webpack的实例,他贯穿了整个webpack的一生,他负责指挥整个打包过程,而Compilation就像是compiler的小兵一样,负责实施构建这一过程。同时他们都是继承自Tapable这一事件流机制。所以他们提供了大量的钩子给用户订阅,订阅不同的钩子就可以传入不同的回调函数,webpack回去逐一执行这些回调。然后改变构建结果
而plugin的hook也是有同步异步之分的。不同的情况下我们需要用不同的方法去订阅。同步的情况下采用tap方法订阅。
compiler.hooks[hookName].tap(PluginName,(params)=>{
/** do some thing */
})
2
3
而异步的情况下又分为tapAsync和tapPromise两种方式,这两种订阅方式的写法也有细微的区别
tapAsync
使用 tapAsync 的时候,我们需要多传入一个 callback 回调,并且在结束的时候一定要调用这个回调告知 webpack 这段异步操作结束了。👇 比如:
class HelloPlugin {
apply(compiler) {
compiler.hooks.emit.tapAsync(HelloPlugin, (compilation, callback) => {
setTimeout(() => {
console.log('async')
callback()
}, 1000)
})
}
}
module.exports = HelloPlugin
2
3
4
5
6
7
8
9
10
11
12
13
tapPromise
当使用 tapPromise 来处理异步的时候,我们需要返回一个 Promise 对象并且让它在结束的时候 resolve 👇
class HelloPlugin {
apply(compiler) {
compiler.hooks.emit.tapPromise(HelloPlugin, (compilation) => {
return new Promise((resolve) => {
setTimeout(() => {
console.log('async')
resolve()
}, 1000)
})
})
}
}
module.exports = HelloPlugin
2
3
4
5
6
7
8
9
10
11
12
13
14
# 实践
下面通过一个小栗子来感受一下自定义plugin
这个插件实现的功能是在打包后输出的文件夹内多增加一个 markdown 文件,文件内记录打包的时间点、文件以及文件大小的输出。
首先我们根据需求确定我们需要的 hook ,由于需要输出文件,我们需要使用 compilation 的 emitAsset 方法。 其次由于需要对 assets 进行处理,所以我们使用 compilation.hooks.processAssets ,因为 processAssets 是负责 asset 处理的钩子。 这样我们插件结构就出来了
class OutLogPlugin {
constructor(options) {
this.outFileName = options.outFileName
}
apply(compiler) {
// 可以从编译器对象访问 webpack 模块实例
// 并且可以保证 webpack 版本正确
const { webpack } = compiler
// 获取 Compilation 后续会用到 Compilation 提供的 stage
const { Compilation } = webpack
const { RawSource } = webpack.sources
/** compiler.hooks.<hoonkName>.tap/tapAsync/tapPromise */
compiler.hooks.compilation.tap('OutLogPlugin', (compilation) => {
compilation.hooks.processAssets.tap(
{
name: 'OutLogPlugin',
// 选择适当的 stage,具体参见:
// https://webpack.js.org/api/compilation-hooks/#list-of-asset-processing-stages
stage: Compilation.PROCESS_ASSETS_STAGE_SUMMARIZE,
},
(assets) => {
let resOutput = `buildTime: ${new Date().toLocaleString()}\n\n`
resOutput += `| fileName | fileSize |\n| --------- | --------- |\n`
Object.entries(assets).forEach(([pathname, source]) => {
resOutput += `| ${pathname} | ${source.size()} bytes |\n`
})
compilation.emitAsset(
`${this.outFileName}.md`,
new RawSource(resOutput),
)
},
)
})
}
}
module.exports = OutLogPlugin
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# 总结
webpack插件主要是利用了webpack的compile
实例和compilation
实例继承自Tapable
类,提供了一系列的钩子可供用户订阅
。用户在订阅
这些钩子的同时传入回调函数
,webpack就会去逐一执行。从而改变
构建结果。订阅方式也分为同步订阅
和异步订阅
。同步订阅采用tap
方法,传入插件名和回调即可。异步订阅
分为tapAsync
和tapPromise
两种方式,
tapAsync
的回调中多接受一个callback
函数,通过调用callback
来告诉webpack
异步完成了。tapPromise
则是在回调函数里返回一个promise
,然后resolve
来告诉webpack异步结束
。