Comipler 是其 webpack 的支柱模块,其继承于 Tapable 类,在 compiler 上定义了很多钩子函数,贯穿其整个编译流程,这些钩子上注册了很多插件,用于在特定的时机执行特定的操作,同时,用户也可以在这些钩子上注册自定义的插件来进行功能拓展,接下来将围绕这些钩子函数来分析 webpack 的主流程。
1. compiler 生成
compiler 对象的生成过程大致可以简化为如下过程,首先对我们传入的配置进行格式验证,接着调用 Compiler 构造函数生成 compiler 实例,自定义的 plugins 注册,最后调用 new WebpackOptionsApply().process(options, compiler)
进行默认插件的注册,comailer 初始化等。
const webpack = (options,callback)=>{
//options 格式验证
const webpackOptionsValidationErrors = validateSchema(
webpackOptionsSchema,
options
);
...
// 生成 compiler 对象
let compiler = new Compiler(options.context);
// 自定义插件注册
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.options = new WebpackOptionsApply().process(options, compiler);
}
2. compiler.run
生成 compler 实例后,cli.js 中就会调用 compiler.run 方法了,compiler.run 的流程大致可以简写如下(去掉错误处理等逻辑),其囊括了整个打包过程,首先依次触发 beforeRun、run 等钩子,接下来调用 compiler.compile()进行编译过程,在回调中取得编译后的 compilation 对象,调用 compiler.emitAssets()输出打包好的文件,最后触发 done 钩子。
run(){const onCompiled = (err, compilation) => {
// 打包输出
this.emitAssets(compilation, err => {this.hooks.done.callAsync(stats)
};
// beforeRun => run => this.compile()
this.hooks.beforeRun.callAsync(this, err => {
this.hooks.run.callAsync(this, err => {
this.readRecords(err => {this.compile(onCompiled);
});
});
});
}
3. compiler.compile
在这个方法中主要也是通过回调触发钩子进行流程控制,通过 newCompilation=>make=>finsih=>seal
流程来完成一次编译过程,compiler 将具体一次编译过程放在了 compilation 实例上,可以将主流程与编译过程分割开来,当处于 watch 模式时,可以进行多次编译。
compile(callback) {const params = this.newCompilationParams();
this.hooks.beforeCompile.callAsync(params, err => {this.hooks.compile.call(params);
const compilation = this.newCompilation(params);
this.hooks.make.callAsync(compilation, err => {
compilation.finish(err => {
compilation.seal(err => {
this.hooks.afterCompile.callAsync(compilation, err => {return callback(null, compilation);
});
});
});
});
});
}
从图中可以看到 make 钩子上注册了 singleEntryPlugin(单入口配置时),compilation 作为参数传入该插件,接着在插件中调用 compilation.addEntry 方法开始编译过程。
compiler.hooks.make.tapAsync(
"SingleEntryPlugin",
(compilation, callback) => {const { entry, name, context} = this;
const dep = SingleEntryPlugin.createDependency(entry, name);
compilation.addEntry(context, dep, name, callback);
}
);
4. compilation 编译过程
编译过程的入口在 compilation._addModuleChain 函数,传入 entry,context 参数,在回调中得到编译生成的 module。编译的过程包括文件和 loader 路径的 resolve,loader 对源文件的处理,递归的进行依赖处理等等,这里不进行详述。
addEntry(context, entry, name, callback) {this.hooks.addEntry.call(entry, name);
this._addModuleChain(
context,
entry,
module => {this.entries.push(module);
},
(err, module) => {this.hooks.succeedEntry.call(entry, name, module);
return callback(null, module);
}
);
}
5. compilation.seal
在 webpack 的工作流程当中,在上一步中得到编译好的所有模块后,会调用回调函数,回调向上传递了好几层,最后调用的是 compiler.compile 中的 compilation.finish 以及 compilation.seal,主要操作在 compilation.seal 中。
主要步骤为:构建 module graph,构建 chunk graph、生成 moduleId,生成 chunkId,生成 hash,然后生成最终输出文件的内容,同时每一步之间都会暴露 hook , 提供给插件修改的机会。
6. compiler.emitAssets
经历了上面所有的阶段之后,所有的最终代码信息已经保存在了 Compilation 的 assets 中, 当 assets 资源相关的优化工作结束后,seal 阶段也就结束了。这时候执行 seal 函数接受到 callback,callback 回溯到 compiler.run 中,执行 compiler.emitAssets.
在这个方法当中首先触发 hooks.emit 钩子函数,即将进行写文件的流程。接下来开始创建目标输出文件夹,并执行 emitFiles 方法,将内存当中保存的 assets 资源输出到目标文件夹当中,这样就完成了内存中保存的 chunk 代码写入至最终的文件
参考资料:
https://juejin.im/post/5d4d08…