Tapable插件结构和Hooks设计
compiler和compilation都继承自Tapable类,Tapable是一个类似于Node的EventEmitter库,主要是控制狗子函数的发布与订阅。
webpack可以理解为一种基于事件流的编程范例,一些列插件的运行。内部插件监听compile或者compilation上面定义的关键事件节点。
const {
SyncHook, //同步钩子
SyncBailHook, //同步熔断钩子
SyncWaterfallHook, //同步流水钩子
SyncLoopHook, //同步循环钩子
AsyncParallelHook, //异步并发钩子
AsyncParallelBailHook, //异步并发熔断钩子
AsyncSeriesHook, //异步串行钩子
AsyncSeriesBailHook, //异步串行熔断钩子
AsyncSeriesWaterfallHook //异步串行流水钩子
} = require("tapable");
Tapable hooks类型
| type | function |
|---|---|
| Hook | 所有钩子的后缀 |
| Waterfall | 流水钩子,执行的结果可以传递给下一个插件 |
| Bail | 熔断钩子,遇到return直接返回 |
| Loop | 监听函数返回true表示继续循环,返回undefined表示结束循环 |
| Sync | 同步方法 |
| AsyncSeries | 异步串行钩子 |
| AsyncParallel | 异步并行钩子 |
Tabpack 提供了同步&异步绑定钩子的方法,并且他们都有绑定(监听)事件和执行(触发)事件对应的方法。
| Async | Sync |
|---|---|
| 监听:tapAsync/tapPromise/tap | 监听:tap |
| 触发:callAsync/promise | 触发:call |
// demo
const {
SyncHook,
AsyncSeriesHook
} = require('tapable');
class Car {
constructor() {
this.hooks = {
accelerate: new SyncHook(['newspeed']),
brake: new SyncHook(),
calculateRoutes: new AsyncSeriesHook(["source", "target", "routesList"])
}
}
}
const myCar = new Car();
//绑定同步钩子
myCar.hooks.brake.tap("WarningLampPlugin", () => console.log('WarningLampPlugin'));
//绑定同步钩子 并传参
myCar.hooks.accelerate.tap("LoggerPlugin", newSpeed => console.log(`Accelerating to ${newSpeed}`));
//绑定一个异步Promise钩子
myCar.hooks.calculateRoutes.tapPromise("calculateRoutes tapPromise", (source, target, routesList, callback) => {
// return a promise
return new Promise((resolve,reject)=>{
setTimeout(()=>{
console.log(`tapPromise to ${source} ${target} ${routesList}`)
resolve();
},1000)
})
});
myCar.hooks.brake.call();
myCar.hooks.accelerate.call(10);
console.time('cost');
//执行异步钩子
myCar.hooks.calculateRoutes.promise('Async', 'hook', 'demo').then(() => {
console.timeEnd('cost');
}, err => {
console.error(err);
console.timeEnd('cost');
});
compiler和compilation
compiler文件中可以看到,定义了很多很多hooks
有流程类的:
· (before-)run
· (before-after-)compile
· make
· (after-)emit
· done
监听类的:
· watch-run
· watch-close
class Compiler extends Tapable {
constructor(context) {
super();
this.hooks = {
...
// 构建完成会走到done钩子
done: new AsyncSeriesHook(["stats"]),
// 构建前
beforeRun: new AsyncSeriesHook(["compiler"]),
// 开始编译
run: new AsyncSeriesHook(["compiler"]),
// compilation和this.compilation,像webpack一些插件,比如htmlwebpackplugin内部也有独立的构建流程
thisCompilation: new SyncHook(["compilation", "params"]),
compilation: new SyncHook(["compilation", "params"]),
// 还有初始化的钩子
entryOption: new SyncBailHook(["context", "entry"])
};
...
}
module.exports = Compiler;
compilation相关hooks,主要负责模块的编译打包优化:
entry相关,(un-)seal是否成功构建,(before-after-)chunks,optimize优化相关,hash相关。
webpack流程的三个阶段:
- 1.准备阶段
- 2.模块打包构建
- 3.模块优化,代码生成,输出到磁盘
webpack流程之准备阶段
- 引用WebpackOptionsDefaulter,设置一些默认参数
- 引用NodeEnvironmentPlugin,清理构建缓存
- 调用了WebpackOptionsApply对象,将options转化插件
// lib下的webpack,即main入口文件
const webpack = (options, callback) => {
const webpackOptionsValidationErrors = validateSchema(
webpackOptionsSchema,
options
);
if (webpackOptionsValidationErrors.length) {
throw new WebpackOptionsValidationError(webpackOptionsValidationErrors);
}
let compiler;
if (Array.isArray(options)) {
compiler = new MultiCompiler(
Array.from(options).map(options => webpack(options))
);
} else if (typeof options === "object") {
// 1.引用WebpackOptionsDefaulter,设置一些默认参数
options = new WebpackOptionsDefaulter().process(options);
compiler = new Compiler(options.context);
compiler.options = options;
//2. 引用NodeEnvironmentPlugin,清理构建缓存
new NodeEnvironmentPlugin({
infrastructureLogging: options.infrastructureLogging
}).apply(compiler);
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);
}
}
}
compiler.hooks.environment.call();
compiler.hooks.afterEnvironment.call();
// 3.调用了WebpackOptionsApply对象
compiler.options = new WebpackOptionsApply().process(options, compiler);
} else {
throw new Error("Invalid argument: options");
}
if (callback) {
if (typeof callback !== "function") {
throw new Error("Invalid argument: callback");
}
if (
options.watch === true ||
(Array.isArray(options) && options.some(o => o.watch))
) {
const watchOptions = Array.isArray(options)
? options.map(o => o.watchOptions || {})
: options.watchOptions || {};
return compiler.watch(watchOptions, callback);
}
compiler.run(callback);
}
return compiler;
};
exports = module.exports = webpack;
exports.version = version;
WebpackOptionsDefaulter文件:
class WebpackOptionsDefaulter extends OptionsDefaulter {
constructor() {
super();
this.set("entry", "./src");
this.set("devtool", "make", options =>
options.mode === "development" ? "eval" : false
);
this.set("output.filename", "[name].js");
this.set("output.chunkFilename", "make", options => {
const filename = options.output.filename;
if (typeof filename !== "function") {
const hasName = filename.includes("[name]");
const hasId = filename.includes("[id]");
const hasChunkHash = filename.includes("[chunkhash]");
// Anything changing depending on chunk is fine
if (hasChunkHash || hasName || hasId) return filename;
// Elsewise prefix "[id]." in front of the basename to make it changing
return filename.replace(/(^|\/)([^/]*(?:\?|$))/, "$1[id].$2");
}
return "[id].js";
});
this.set("output.library", "");
this.set("output.hotUpdateFunction", "make", options => {
return Template.toIdentifier(
"webpackHotUpdate" + Template.toIdentifier(options.output.library)
);
});
this.set("output.jsonpFunction", "make", options => {
return Template.toIdentifier(
"webpackJsonp" + Template.toIdentifier(options.output.library)
);
});
...
NodeEnvironmentPlugin文件:
// run之前的清构建缓存的一个处理
compiler.hooks.beforeRun.tap("NodeEnvironmentPlugin", compiler => {
if (compiler.inputFileSystem === inputFileSystem) inputFileSystem.purge();
});
WebpackOptionsApply
作用:将所有的配置options参数,转换成webpack内部插件:
output.library -> LibraryTemplatePlugin
externals -> ExternalsPlugin
devtool -> EvalDevtoolModulePlugin,SourceMapDevToolPlugin
触发后哪些地方监听
class WebpackOptionsApply extends OptionsApply {
constructor() {
super();
}
process(options, compiler) {
compiler.dependencies = options.dependencies;
// 设置options.target为web后,WebpackOptionsApply会依次加载需要的插件
if (typeof options.target === "string") {
...
switch (options.target) {
case "web":
JsonpTemplatePlugin = require("./web/JsonpTemplatePlugin");
FetchCompileWasmTemplatePlugin = require("./web/FetchCompileWasmTemplatePlugin");
NodeSourcePlugin = require("./node/NodeSourcePlugin");
// 通过apply方法,插件都会变成compiler上的一个实例
new JsonpTemplatePlugin().apply(compiler);
new FetchCompileWasmTemplatePlugin({
mangleImports: options.optimization.mangleWasmImports
}).apply(compiler);
new FunctionModulePlugin().apply(compiler);
new NodeSourcePlugin(options.node).apply(compiler);
new LoaderTargetPlugin(options.target).apply(compiler);
break;
...
WebpackOptionsApply的另一个作用:默认加载了EntryOptionPlugin
// WebpackOptionsApply.js
new EntryOptionPlugin().apply(compiler);
// 触发entryOption这个hook
compiler.hooks.entryOption.call(options.context, options.entry);
执行命令:grep "entryOption" -rn ./node_modules/webpack,看下哪些地方监听了
查找结果:
可以看到,是EntryOptionPlugin.js
// EntryOptionPlugin.js
const SingleEntryPlugin = require("webpack/lib/SingleEntryPlugin");
const MultiEntryPlugin = require("webpack/lib/MultiEntryPlugin");
const DynamicEntryPlugin = require("webpack/lib/DynamicEntryPlugin");
const itemToPlugin = (context, item, name) => {
if (Array.isArray(item)) {
// 数组会转成多个entry
return new MultiEntryPlugin(context, item, name);
}
// 对象会单个处理
return new SingleEntryPlugin(context, item, name);
};
module.exports = class EntryOptionPlugin {
apply(compiler) {
compiler.hooks.entryOption.tap("EntryOptionPlugin", (context, entry) => {
// 针对不同的entry写入,有不同的处理
// 这就是为什么entry可以是数组,可以是对象,可以是个func
if (typeof entry === "string" || Array.isArray(entry)) {
itemToPlugin(context, entry, "main").apply(compiler);
} else if (typeof entry === "object") {
for (const name of Object.keys(entry)) {
itemToPlugin(context, entry[name], name).apply(compiler);
}
} else if (typeof entry === "function") {
new DynamicEntryPlugin(context, entry).apply(compiler);
}
return true;
});
}
};
完成准备阶段,我们再次回到Compiler中,这里会创造一个Compilation对象和NormalModuleFactory、ContextModuleFactory工厂方法,之后会走到run方法。
// Compiler.js
run(callback) {
if (this.running) return callback(new ConcurrentCompilationError());
const finalCallback = (err, stats) => {
...
};
const startTime = Date.now();
this.running = true;
const onCompiled = (err, compilation) => {
if (err) return finalCallback(err);
// 4.触发shouldEmit,判断资源是否生成
if (this.hooks.shouldEmit.call(compilation) === false) {
const stats = new Stats(compilation);
stats.startTime = startTime;
stats.endTime = Date.now();
this.hooks.done.callAsync(stats, err => {
if (err) return finalCallback(err);
return finalCallback(null, stats);
});
return;
}
// 5.到构建环节
this.emitAssets(compilation, err => {
if (err) return finalCallback(err);
if (compilation.hooks.needAdditionalPass.call()) {
compilation.needAdditionalPass = true;
const stats = new Stats(compilation);
stats.startTime = startTime;
stats.endTime = Date.now();
this.hooks.done.callAsync(stats, err => {
if (err) return finalCallback(err);
this.hooks.additionalPass.callAsync(err => {
if (err) return finalCallback(err);
this.compile(onCompiled);
});
});
return;
}
this.emitRecords(err => {
if (err) return finalCallback(err);
const stats = new Stats(compilation);
stats.startTime = startTime;
stats.endTime = Date.now();
this.hooks.done.callAsync(stats, err => {
if (err) return finalCallback(err);
return finalCallback(null, stats);
});
});
});
};
// 1.在执行run之前,先执行了beforeRun这个钩子
// 注意:这个钩子就是在上面NodeEnvironmentPlugin文件中绑定的,做了一个清除构建缓存的处理
this.hooks.beforeRun.callAsync(this, err => {
if (err) return finalCallback(err);
// 2.执行完beforeRun,进入到run钩子中
this.hooks.run.callAsync(this, err => {
if (err) return finalCallback(err);
this.readRecords(err => {
if (err) return finalCallback(err);
// 3.执行onCompiled回调
this.compile(onCompiled);
});
});
});
}
seal:构建完成后,将内容输出,资源生成,优化
webpack流程之模块打包构建阶段
make阶段
继续看compiler文件:
newCompilationParams() {
const params = {
normalModuleFactory: this.createNormalModuleFactory(),
contextModuleFactory: this.createContextModuleFactory(),
compilationDependencies: new Set()
};
return params;
}
compile(callback) {
// 创建了createNormalModuleFactory和createContextModuleFactory两个工厂类
const params = this.newCompilationParams();
this.hooks.beforeCompile.callAsync(params, err => {
if (err) return callback(err);
this.hooks.compile.call(params);
const compilation = this.newCompilation(params);
// 执行make钩子
this.hooks.make.callAsync(compilation, err => {
if (err) return callback(err);
compilation.finish(err => {
if (err) return callback(err);
compilation.seal(err => {
if (err) return callback(err);
this.hooks.afterCompile.callAsync(compilation, err => {
if (err) return callback(err);
return callback(null, compilation);
});
});
});
});
});
}
执行命令:查找一下哪里监听make:grep "hooks.make" -rn "./node_modules/webpack"
以SingleEntryPlugin.js为例:
// SingleEntryPlugin.js
compiler.hooks.make.tapAsync(
"SingleEntryPlugin",
(compilation, callback) => {
const { entry, name, context } = this;
const dep = SingleEntryPlugin.createDependency(entry, name);
// 往配置中添加入口,make构建阶段正式开始
compilation.addEntry(context, dep, name, callback);
}
);
build流程
// Compilation.js
buildModule(module, optional, origin, dependencies, thisCallback) {
...
// 1.触发buildModule
this.hooks.buildModule.call(module);
// 2.调用normalModule中的buildMoule?buildMoule调用了doBuild
module.build(
this.options,
this,
this.resolverFactory.get("normal", module.resolveOptions),
this.inputFileSystem,
error => {
...
if (error) {
// 6.2失败触发failedModule
this.hooks.failedModule.call(module, error);
return callback(error);
}
// 6.1成功触发succeedModule,构建结果都放在了this.modules数组中
this.hooks.succeedModule.call(module);
return callback();
}
);
}
// normalModule。j s
const { getContext, runLoaders } = require("loader-runner");
build(options, compilation, resolver, fs, callback) {
...
// 3.执行doBuild方法
return this.doBuild(options, compilation, resolver, fs, err => {
...
try {
// 5.构建完成后会运行这个parser,parse中用的是acorn
// 作用是把js中require的依赖添加到依赖列表中
const result = this.parser.parse(
this._ast || this._source.source(),
{
current: this,
module: this,
compilation: compilation,
options: options
},
(err, result) => {
if (err) {
handleParseError(err);
} else {
handleParseResult(result);
}
}
);
if (result !== undefined) {
// parse is sync
handleParseResult(result);
}
} catch (e) {
handleParseError(e);
}
});
}
doBuild(options, compilation, resolver, fs, callback) {
const loaderContext = this.createLoaderContext(
resolver,
options,
compilation,
fs
);
// 依赖runLoaders包去执行loader
runLoaders(
{
// 传递静态资源的路径
resource: this.resource,
loaders: this.loaders,
context: loaderContext,
readResource: fs.readFile.bind(fs)
},
...
}
seal阶段
class Compilation extends Tapable {
constructor(compiler) {
super();
this.hooks = {
/** @type {SyncHook} */
optimize: new SyncHook([]),
/** @type {SyncBailHook<Module[]>} */
optimizeModulesBasic: new SyncBailHook(["modules"]),
/** @type {SyncBailHook<Module[]>} */
optimizeModules: new SyncBailHook(["modules"]),
/** @type {SyncBailHook<Module[]>} */
optimizeModulesAdvanced: new SyncBailHook(["modules"]),
/** @type {SyncHook<Module[]>} */
afterOptimizeModules: new SyncHook(["modules"]),
/** @type {SyncBailHook<Chunk[], ChunkGroup[]>} */
optimizeChunksBasic: new SyncBailHook(["chunks", "chunkGroups"]),
/** @type {SyncBailHook<Chunk[], ChunkGroup[]>} */
optimizeChunks: new SyncBailHook(["chunks", "chunkGroups"]),
/** @type {SyncBailHook<Chunk[], ChunkGroup[]>} */
optimizeChunksAdvanced: new SyncBailHook(["chunks", "chunkGroups"]),
/** @type {SyncHook<Chunk[], ChunkGroup[]>} */
afterOptimizeChunks: new SyncHook(["chunks", "chunkGroups"]),
...
模块构建好后,会触发compilation中的finish方法?
webpack流程之文件生成
入口文件addEntry -> _addModuleChain -> buildModule -> seal -> createHash -> createModuleAssets
_addModuleChain: 分析入口文件依赖的模块,将模块添加到依赖列表中,同时进行模块构建,执行buildModule
createModuleAssets:
// compilation.js
createModuleAssets() {
for (let i = 0; i < this.modules.length; i++) {
const module = this.modules[i];
if (module.buildInfo.assets) {
const assetsInfo = module.buildInfo.assetsInfo;
for (const assetName of Object.keys(module.buildInfo.assets)) {
const fileName = this.getPath(assetName);
// 将构建好的module放到asset,
this.emitAsset(
fileName,
module.buildInfo.assets[assetName],
assetsInfo ? assetsInfo.get(assetName) : undefined
);
this.hooks.moduleAsset.call(module, fileName);
}
}
}
}
最后到emit阶段,是在compile
// compile.js
this.hooks.emit.callAsync(compilation, err => {
if (err) return callback(err);
// compilation获取输出的内容
outputPath = compilation.getPath(this.outputPath);
// 输出的内容放到磁盘中
this.outputFileSystem.mkdirp(outputPath, emitFiles);
});