术语
hook和tap method
其实,hook就像DOM事件系统里面的【事件】,而tap method就是我们用于监听事件的addEventListener。
compiler.hooks.beforeRun.tap('pluginName',callback);
以上的代码可以类比地理解为:
compiler.hooks.addEventListener('beforeRun',callback)
什么是webpack plugin?
从实现的角度来说,webpack plugin就是一个有apply方法的javascrit对象。跟react组件必须实现render方法一样,这个javascript对象必须实现apply方法。不实现的话,那么webpack是会报错的:
webpack的运行期本质上就是webpack compiler的生命周期。在webpack compiler的生命周期过程总,它会借助tapable框架来向外界发射不同事件。开发者可以通过plugin hook进这些不同的事件节点来执行自己想要的操作,从而达到扩展webpack功能的目的。
从理论上来说,plugin是webpack这个bundler的基石。因为就是连webpack本身也是基于同一个plugin系统所打造出来的。
与此同时,plugin机制独立存在的另外一个理由是,它能够做一些webpack loader所不能做的事情。
以上都是理论上的东西,那webpack plugin到底长什么样的呢?一个简单的useless的plugin可以是这样的:
const pluginName = 'ConsoleLogOnBuildWebpackPlugin';
class ConsoleLogOnBuildWebpackPlugin {
apply(compiler) {
compiler.hooks.beforeRun.tap(pluginName, (compiler) => {
console.log('准备编译');
});
}
}
module.exports = ConsoleLogOnBuildWebpackPlugin;
然后在使用的时候,把它放在plugins数组里面:
const path = require('path');
const ConsoleLogOnBuildWebpackPlugin = require('./ConsoleLogOnBuildWebpackPlugin');
module.exports = {
mode:'development',
entry: './index.js',
output: {
path: path.resolve(__dirname,'dist'),
filename: 'my-first-webpack-bundle.js'
},
plugins:[
new ConsoleLogOnBuildWebpackPlugin()
]
}
这里值得指出的是,大部分主流的plugin都是实现为一个class,通过new操作符调用得到一个实例传入到plugins数组里面。其实这不是必须的。就像上面说的,我们只需要保证plugin是一个对象,并且有apply方法即可。实现为class的主要原因是为了支持使用通过传参来进行自定义配置。
从上面,我们可以简单看出webpack plugin的基本架构这样的:
class PluginName {
apply(compiler) {
// do something
}
}
plugin API架构
plugin监听的事件是由hooks对象所提供的。而hooks对象又是关联到某个功能模块的。还是拿上面的useless plugin举例说明:
compiler.hooks.beforeRun.tap('pluginName',callback);
在上面示例代码中,beforeRun事件是由hooks对象所提供的,而hooks对象是又关联到compiler实例这个功能模块上来的。因此,如果从hooks对象所关联的功能模块来看的话,那么hook又可以分为:
- compiler hook
- compilation hook
- parser hook
- resolver hook
同时,compiler,comilation,parser代表的是webpack运行时所经历的三个阶段。
compiler hook
compiler模块是创建接下来的compilation实例的引擎。在compiler阶段开始的时候,它会使用【通过CLI或者Node API传进来的参数】来完成compilation实例的创建。compiler阶段覆盖整个webpack运行期。compiler模块通过继承Tapable类来提供plugin的注册和调用功能。compiler hook就是指compiler阶段所支持hook。在webpack@4.43里面,compiler阶段共支持27个hook:
shouldEmit
done
additionalPass
beforeRun
run
emit
assetEmitted
afterEmit
thisCompilation
compilation
normalModuleFactory
contextModuleFactory
beforeCompile
compile
make
afterCompile
watchRun
failed
invalid
watchClose
infrastructureLog
environment
afterEnvironment
afterPlugins
afterResolvers
entryOption
infrastructurelog
以上的排列顺序并不是它的触发顺序。触发顺序以及每个hook的具体含义请查看官网。
compilation hook
compiler每进行一次编译的时候,它就会创建一个complilation实例,用于整个的编译阶段。通过complilation实例我们可以访问到所有的模块和它们的依赖模块。在compilation阶段,模块会被加载,冻结(sealed),优化,分包(chunked),哈希运算(hashed)和重新存储(restored)。compilation阶段所发射的事件就是compilation hook。在webpack@4.43里面,compilation阶段共支持76个hook:
buildModule
rebuildModule
failedModule
succeedModule
addEntry
failedEntry
succeedEntry
dependencyReference
finishModules
finishRebuildingModule
unseal
seal
beforeChunks
afterChunks
optimizeDependenciesBasic
optimizeDependencies
optimizeDependenciesAdvanced
afterOptimizeDependencies
optimize
optimizeModulesBasic
optimizeModules
optimizeModulesAdvanced
afterOptimizeModules
optimizeChunksBasic
optimizeChunks
optimizeChunksAdvanced
afterOptimizeChunks
optimizeTree
afterOptimizeTree
optimizeChunkModulesBasic
optimizeChunkModules
optimizeChunkModulesAdvanced
afterOptimizeChunkModules
shouldRecord
reviveModules
optimizeModuleOrder
advancedOptimizeModuleOrder
beforeModuleIds
moduleIds
optimizeModuleIds
afterOptimizeModuleIds
reviveChunks
optimizeChunkOrder
beforeChunkIds
optimizeChunkIds
afterOptimizeChunkIds
recordModules
recordChunks
beforeHash
contentHash
afterHash
recordHash
record
beforeModuleAssets
shouldGenerateChunkAssets
beforeChunkAssets
additionalChunkAssets
additionalAssets
optimizeChunkAssets
afterOptimizeChunkAssets
optimizeAssets
afterOptimizeAssets
needAdditionalSeal
afterSeal
chunkHash
moduleAsset
chunkAsset
assetPath
needAdditionalPass
childCompiler
log
normalModuleLoader
optimizeExtractedChunksBasic
optimizeExtractedChunks
optimizeExtractedChunksAdvanced
afterOptimizeExtractedChunks
以上的排列顺序并不是它的触发顺序。触发顺序以及每个hook的具体含义请查看官网。
parser hook
parser实例也是由compiler来创建的。在parser阶段,它主要是用于解析模块,包括生成AST(abstract syntax tree)对象和根据import/require来生成模块依赖对象(dependency object)。parser阶段所发射的事件就是parser hook。在webpack@4.43里面,parser阶段共支持34个hook:
evaluateTypeof
evaluate
evaluateIdentifier
evaluateDefinedIdentifier
evaluateCallExpressionMember
statement
statementIf
label
import
importSpecifier
export
exportImport
exportDeclaration
exportExpression
exportSpecifier
exportImportSpecifier
varDeclaration
varDeclarationLet
varDeclarationConst
varDeclarationVar
canRename
rename
assigned
assign
typeof
importCall
call
callAnyMember
new
expression
expressionAnyMember
expressionConditionalOperator
expressionLogicalOperator
program
以上的排列顺序并不是它的触发顺序。触发顺序以及每个hook的具体含义请查看官网。
Resolver hook
在自定义webpack plugin的时候,Resolver hook不太常用,有需要了解可以去查看官方文档。
hook的类型
以上几个功能模块都是通过继承tapable类库不同hook子类来向外界提供hook能力的。官方文档罗列的子类如下:
const {
SyncHook,
SyncBailHook,
SyncWaterfallHook,
SyncLoopHook,
AsyncParallelHook,
AsyncParallelBailHook,
AsyncSeriesHook,
AsyncSeriesBailHook,
AsyncSeriesWaterfallHook
} = require("tapable");
所以,我们可以将hook大致分为两种类型:同步hook(sync hook)和异步的hook(async hook)。
之所以,做出以上的区分,是因为不同类型的hook,使用的tap方法是不一样的。tapable类库支持的tap方法有tap,tapAsync和tapPromise三种。如果一个hook是同步hook,那么它只能使用tap这个方法;而如果一个hook是异步hook,以上的这三种tap方法都是可以使用的。
编写自己的webpack plugin
webpack plugin的大体架构上面已经给出来,我们主要在apply方法填充我们的方法即可。具体的做法,根据自己的目的来找到对应的hook,通过注册一个callback来执行自己想要的操作。
比如说,给文件名加入fingerprint之后,多次改动并打包会导致打包目录堆叠着一些旧的bundle文件,这个时候,我们就可以编写一个webpack plugin来在webpack发射资源之前先把旧的bundle文件删除掉。我们命名为WebpackCleanPlugin。实现这个plugin,其实也很简单。因为我们已经知道了plugin的基本架构。所以,我们可以写出以下代码:
const pluginName = 'WebpackCleanPlugin';
class WebpackCleanPlugin {
apply(compiler) {
}
}
接下来,就是填充apply方法。具体的步骤是,先找到合适的hook,再在hook的callback里面实现文件夹清理功能。这里,我们不妨选取监听compiler的第一个hookentryOption。
const pluginName = 'WebpackCleanPlugin';
class WebpackCleanPlugin {
apply(compiler) {
compiler.hooks.entryOption.tap(pluginName, (context, entry) => {
const outputPath = compiler.options.output.path;
});
}
}
上面代码,我们从compiler实例身上获取到用户在webpack配置中所配置的output文件夹的路径。接下来,新建一个cleanDir.js,实现文件夹的清理功能:
// cleanDir.js
const fs = require('fs');
const path = require('path');
function cleanDir(dirPath, depth = 0) {
fs.readdirSync(dirPath).forEach((file, index) => {
const filePath = path.join(dirPath, file);
if (fs.lstatSync(filePath).isDirectory()) {
const nextDepth = depth + 1;
cleanDir(filePath, nextDepth);
} else {
fs.unlinkSync(filePath);
}
})
if (depth > 0) {
fs.rmdirSync(dirPath); // 删除文件夹本身
}
};
module.exports = cleanDir;
然后,我们在hook的callback里面直接使用就可以了:
const clearDir = require('./cleanDir');
const chalk = require('chalk');
const pluginName = 'WebpackCleanPlugin';
class WebpackCleanPlugin {
apply(compiler) {
compiler.hooks.entryOption.tap(pluginName, (context, entry) => {
const outputPath = compiler.options.output.path;
clearDir(outputPath);
// 因为我们的清除过程是同步的,所以,在这里可以打印以下提示语:
console.log(chalk.green('打包目录清理完毕'));
});
}
}
以上就是实现一个简单的webpack plugin的过程。可以看出,编写一个webpack plugin主要包含三个步骤:
- 找到相应的hook;
- 从compiler实例,compilation实例和parser实例上获取相关的数据;
- 围绕Node API来消费这些数据,并执行自己想要的操作(主要是副作用类型的操作)。