乐趣区

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 代码写入至最终的文件

参考资料:

https://juejin.im/post/5d4d08…

退出移动版