Webpack

110 阅读8分钟

webpack打包流程

2. webpack事件流 && 钩子

webpack事件流可以认为是webpack在构建过程中会在对应的编译阶段触发的一系列事件,而这些事件通过webpack广播后,plugin插件监听到对应事件名,拿到当前阶段的文件资源,通过回调函数执行特殊功能处理。在整个webapck编译的事件流中都是通过Tapable进行管理。

2.0 tabpable原理

Tapable的原理其实就是我们在前端进阶过程中都会经历的EventEmit,通过发布者-订阅者模式实现,它的部分核心代码可以概括成下面这样:

class SyncHook{
    constructor(){
        this.hooks = [];
    }

    // 订阅事件
    tap(name, fn){
        this.hooks.push(fn);
    }

    // 发布
    call(){
        this.hooks.forEach(hook => hook(...arguments));
    }
}

webpack事件流中compiler主要的钩子,如下图 :

ps.这里借用一下juejin.cn/post/692049… 大佬的图哈

webpack事件流主要分为四个阶段:

  1. 初始化阶段:创建编译器
  2. 编译阶段:编译器运行
  3. 输出阶段:编译器输出 image.png

2.1 初始化阶段:创建编译器

compiler = createCompiler(options);   // 创建compiler
const createCompiler = rawOptions => {
	const options = getNormalizedWebpackOptions(rawOptions);
	applyWebpackOptionsBaseDefaults(options);
	const compiler = new Compiler(options.context);
	compiler.options = options;
	new NodeEnvironmentPlugin({
		infrastructureLogging: options.infrastructureLogging
	}).apply(compiler);
	if (Array.isArray(options.plugins)) {
		for (const plugin of options.plugins) {
			if (typeof plugin === "function") {
				plugin.call(compiler, compiler);
			} else {
				plugin.apply(compiler);
			}
		}
	}
	applyWebpackOptionsDefaults(options);
        
        // 注意:执行完后触发三个钩子-API:environment、afterEnvironment、initialize
	compiler.hooks.environment.call();
	compiler.hooks.afterEnvironment.call();
	new WebpackOptionsApply().process(options, compiler);
	compiler.hooks.initialize.call();
	return compiler;
};

初始化阶段
(1)初始化options配置:webpack会读取你在的shell命令行传入的配置和项目中的webpack.config.js文件中的配置(如果配置文件中存在plugin实例化语句也会一并执行:new plugin().),初始化本次构建所需要的配置参数;
(2)生成compiler实例:得到上一阶段完整的配置化参数后,生成compiler实例(上一阶段的实例也会被放置在compiler实例中)
(3)依次执行plugin的apply:生成compiler实例后,就开始为webpack事件流上挂载对应的plugin钩子,即执行各个插件实例的apply方法(注意:apply方法中传入compiler实例,并且在钩子回调中传入compileration实例用来拿到此阶段编译资源)。
(4)为complier配置node环境方便读取文件
(5)entry-options:即开始读取配置中的entry参数,递归遍历所有入口

2.2 编译阶段:编译器运行编译阶段

const { compiler, watch, watchOptions } = create(); // 1.创建编译器阶段
if (watch) {
    compiler.watch(watchOptions, callback);
} else {
    compiler.run((err, stats) => {                 // 2.如果没有开启watch,则运行编译器对象的逻辑
        compiler.close(err2 => {
            callback(err || err2, stats);
        });
    });
}

进入compiler编译器实例的编辑运行阶段,执行run函数。注意只是进入到compiler实例的运行阶段,不是编译阶段,编译阶段是中编译开始的标志是make函数的执行。而这之间还有好多函数要处理。

run函数逻辑
1.触发beforeRun钩子
2.触发run钩子
3.执行compile函数

const run = () => {
    this.hooks.beforeRun.callAsync(this, err => { //  1.执行beforeRun钩子
         if (err) return finalCallback(err);
         this.hooks.run.callAsync(this, err => { //   2.执行run钩子
              if (err) return finalCallback(err);
              this.readRecords(err => {
                   if (err) return finalCallback(err);
                   this.compile(onCompiled); //       3.执行compile()编译函数
               });
         });
    });
};

compile()编译函数
1.先执行beforeCompile的async钩子,在这个钩子的回调函数中执行make钩子。
2.然后再执行make函数后,此时进入到真正的编译流程(从entry入口出发,根据文件类型和配置loader进行编译)
3.编译完成后执行finishMake钩子(传入compileration对象)
4.compilation.finish
5.compilation.seal(生成chunk)
6.hooks.afterCompile钩子

compile(callback) {
        const params = this.newCompilationParams();
        this.hooks.beforeCompile.callAsync(params, err => {
                const compilation = this.newCompilation(params); //beforecompiler函数中新建了compileration类,compileration专门用来编译源码,创建模块,compiler用来控制流程
                this.hooks.make.callAsync(compilation, err => { // 执行make开始正式启动编译,当make执行完之后就表示编译结束了,可以对文件进行seal封装了
                        ...
                        this.hooks.finishMake.callAsync(compilation, err => {
                                ...
                                process.nextTick(() => {
                                        ...
                                        compilation.finish(err => {
                                                ...
                                                compilation.seal(err => {
                                                        ...
                                                            this.hooks.afterCompile.callAsync(compilation, err => {
                                                                logger.timeEnd("afterCompile hook");
                                                                if (err) return callback(err);

                                                                return callback(null, compilation);
                                                        });
                                                });
                                        });
                                });
                        });
                });
        });
}

Compiler控制流程,Compilation专业解析,ModuleFactory生成模块,Parser解析源码,最后通过Template组合模块,输出打包文件的过程
编辑阶段
(1)执行run方法
(2)执行compile方法开始编译:触发五个钩子beforeCompile,compile,makefinishMakeafterCompile,当执行到make钩子之后就是开始真正的编译了。
(3)使用loader对文件进行编译,再将编译好的内容通过acron生成静态AST树,递归分析文件依赖并拉取。最后将所有模块中的require语法替换成_webpack_require_来实现模块化操作。

Compiler.run()函数 主要钩子:

  1. compiler.hooks.beforeRun;
  2. comiler.hooks.run: 这个钩子主要是处理缓存的模块,通过缓存的模块就可以减少需要编译的模块,进而加快编译的速度,之后进入this.compile()函数

Compiler.compile()函数 主要钩子

  1. beforeCompile
  2. compile
  3. make(真正的编译,非常重要)
  4. finishMake
  5. afterCompile

总结:
梳理一下编译阶段compiler对象的函数执行栈: compiler.run-> compiler.compile -> compiler.onCompiled

  • run函数中触发的钩子:beforeRun,run
  • compile函数中触发的钩子:beforeCompile,compile,thisCompilation,compilation,make,afterCompile
  • onCompiled函数中触发的钩子: should-emit,emit,done。

1.讲一下make钩子的过程:

compiler.hooks.make.tapAsync(
        "SingleEntryPlugin", 
        (compilation, callback) => { 
            const { entry, name, context } = this;
            const dep = SingleEntryPlugin.createDependency(entry, name);  
            compilation.addEntry(context, dep, name, callback);
         });
)

1.预处理:先根据是单入口还是多入口,如果是单入口的话调用SingleEntryPlugin钩子,多入口调用mulpileEntryPlugin钩子。通过这类钩子的主要作用要是与模块类形成一个key-value对,如果是单入口的话就就使用单入口对应的模块类创建模块,如果是多入口的话就用多入口模块类创建模块,如果是动态加载就用动态模块类创建模块实例。将这个key-value存储到dep中。
2.执行addEntry函数:
2.1 调用_addModuleChai函数(目的:调用loader,构建模块,寻找依赖,最终形成模块链) 通过dep中对应的模块类构建模块对象,然后调用对象实例的build方法:调用loader,构建模块,寻找依赖。 递归寻找解析,直到所有模块构建完毕,形成模块链。
至此,make编译结束,make钩子回调中调用了compilation的seal方法。开始执行seal钩子,进行chunk构建和打包优化。
注意:这里loader调用时通过dobuild方法来执行runloders去使用loader解析文件内容为js内容(css。vue生成标准的js模块),laoder解析完之后会使用parse生成ast语法树

  • 第一步是调用 loaders 对模块的原始代码进行编译,转换成标准的JS代码

  • 第二步是调用 acorn 对JS代码进行语法分析,然后收集其中的依赖关系。每个模块都会记录自己的依赖关系,从而形成一颗关系树

  • 最后调用 compilation.seal 进入 render 阶段,根据之前收集的依赖,决定生成多少文件,每个文件的内容是什么

const { runLoaders } = require("loader-runner"); 
doBuild(options, compilation, resolver, fs, callback){ 
// runLoaders从包'loader-runner'引入的方法 
runLoaders({ 
    resource: this.resource, // 这里的resource可能是js文件,可能是css文件,可能是img文件
    loaders: this.loaders, 
 }, (err, result) => { 
     const source = result[0]; 
     const sourceMap = result.length >= 1 ? result[1] : null; 
     const extraInfo = result.length >= 2 ? result[2] : null; // ... 
  })
 }

2.seal钩子的过程:chunk构建和打包优化

  1. 调用seal钩子,seal方法中钩子可谓大展威力,seal方法只是调用各种钩子,真正的构建和优化工作,都是插件做的。
  2. 循环调用optimizeDependenciesBasic、optimizeDependencies、optimizeDependenciesAdvanced钩子。
  3. 调用afterOptimizeDependencies钩子。
  4. 调用beforeChunks钩子。从名字也可以看出上面做的依赖方面的优化,此处开始构建Chunk了。
  5. 调用afterChunks钩子。
  6. 调用optimize钩子。
  7. 调用optimizeModulesBasic、optimizeModules、optimizeModulesAdvanced、afterOptimizeModules钩子。此处优化module。,参数是this.modules。
  8. 调用optimizeChunksBasic、optimizeChunks、optimizeChunksAdvanced、afterOptimizeChunks。此处优化Chunk,参数是this.chunkGroups。
  9. 一系列钩子调用不再赘述,有需要的时候,我们再研究。 根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会

总之seal方法完成了Chunk的构建和依赖、Chunk、module等各方面的优化。 (juejin.cn/post/684490…

回到compiler

seal方法执行完毕,生成好了Chunks对象,compilation的工作告一段落,控制权又还给compiler,此时compiler的compile方法就执行完毕了。该执行compile的回调函数onCompiled了。上面我们也贴出来onCompiled的简要代码

onCompiled方法做的事

onCompiled完成了最后的输出阶段。将我们生成的Chunk输出到磁盘上。调用了done钩子之后,一次构建就此完成。

总结、compilation对象的职责

总结:构建模块和Chunk,并利用插件优化构建过程

2.3 输出阶段: 编译器输出阶段

输出阶段
(1)所有文件的编译及转化都已经完成,包含了最终输出的资源,我们可以在传入事件回调的compilation.assets 上拿到所需数据,其中包括即将输出的资源、代码块Chunk等等信息。 shouldEmit、done
emit、afterEmit

image.png