webpack源码阅读之主流程分析
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 代码写入至最终的文件
参考资料: