Webpack原理与实践(一):打包流程

8,123 阅读6分钟

写在前面的话

在阅读 webpack4.x 源码的过程中,参考了《深入浅出webpack》一书和众多大神的文章,结合自己的一点体会,总结如下。

总述

webpack 就像一条生产线,要经过一系列处理流程后才能将源文件转换成输出结果。 这条生产线上的每个处理流程的职责都是单一的,多个流程之间有存在依赖关系,只有完成当前处理后才能交给下一个流程去处理。 插件就像是一个插入到生产线中的一个功能,在特定的时机对生产线上的资源做处理。 webpack 通过 Tapable 来组织这条复杂的生产线。 webpack 在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条生产线中,去改变生产线的运作。 webpack 的事件流机制保证了插件的有序性,使得整个系统扩展性很好。 --吴浩麟《深入浅出webpack》

核心的概念

entryloaderpluginmodulechunk 不论文档还是相关的介绍都很多了,不赘述,有疑问的移步文档。

构建流程

webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:

  1. 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数;
  2. 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译;
  3. 确定入口:根据配置中的 entry 找出所有的入口文件
  4. 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理;
  5. 完成模块编译:在经过第4步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系;
  6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会;
  7. 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。 在以上过程中,webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 webpack 提供的 API 改变 webpack 的运行结果。

webpack 中比较核心的两个对象

  • Compile 对象:负责文件监听和启动编译。Compiler 实例中包含了完整的 webpack 配置,全局只有一个 Compiler 实例。
  • compilation 对象:当 webpack 以开发模式运行时,每当检测到文件变化,一次新的 Compilation 将被创建。一个 Compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等。Compilation 对象也提供了很多事件回调供插件做扩展。
  • 这两个对象都继承自 Tapable。以Compile为例
const {
	Tapable,
	SyncHook,
	SyncBailHook,
	AsyncParallelHook,
	AsyncSeriesHook
} = require("tapable");

class Compiler extends Tapable {
	constructor(context) {
		super();
		this.hooks = {
			/** @type {SyncBailHook<Compilation>} */
			//所有需要输出的文件已经生成好,询问插件哪些文件需要输出,哪些不需要。
			shouldEmit: new SyncBailHook(["compilation"]),
			/** @type {AsyncSeriesHook<Stats>} */
			//成功完成一次完成的编译和输出流程。
			done: new AsyncSeriesHook(["stats"]),
			/** @type {AsyncSeriesHook<>} */
			additionalPass: new AsyncSeriesHook([]),
			/** @type {AsyncSeriesHook<Compiler>} */
			beforeRun: new AsyncSeriesHook(["compiler"]),
			/** @type {AsyncSeriesHook<Compiler>} */
			//启动一次新的编译
			run: new AsyncSeriesHook(["compiler"]),
			/** @type {AsyncSeriesHook<Compilation>} */
			// 确定好要输出哪些文件后,执行文件输出,可以在这里获取和修改输出内容。
			emit: new AsyncSeriesHook(["compilation"]),
			/** @type {AsyncSeriesHook<Compilation>} */
			// 输出完毕
			afterEmit: new AsyncSeriesHook(["compilation"]),
                         // 以上几个事件(除了run,beforerun为编译阶段)其余为输出阶段的事件
			/** @type {SyncHook<Compilation, CompilationParams>} */
			// compilation 创建之前挂载插件的过程
			thisCompilation: new SyncHook(["compilation", "params"]),
			/** @type {SyncHook<Compilation, CompilationParams>} */
			// 创建compilation对象
			compilation: new SyncHook(["compilation", "params"]),
			/** @type {SyncHook<NormalModuleFactory>} */
		    // 初始化阶段:初始化compilation参数
			normalModuleFactory: new SyncHook(["normalModuleFactory"]),
			/** @type {SyncHook<ContextModuleFactory>}  */
		    // 初始化阶段:初始化compilation参数
			contextModuleFactory: new SyncHook(["contextModulefactory"]),

			/** @type {AsyncSeriesHook<CompilationParams>} */
			beforeCompile: new AsyncSeriesHook(["params"]),
			/** @type {SyncHook<CompilationParams>} */
			// 该事件是为了告诉插件一次新的编译将要启动,同时会给插件带上 compiler 对象
			compile: new SyncHook(["params"]),
			/** @type {AsyncParallelHook<Compilation>} */
			//一个新的 Compilation 创建完毕,即将从 Entry 开始读取文件,根据文件类型和配置的 Loader 对文件进行编译,编译完后再找出该文件依赖的文件,递归的编译和解析。
			make: new AsyncParallelHook(["compilation"]),
			/** @type {AsyncSeriesHook<Compilation>} */
		    // 一次Compilation执行完成
			afterCompile: new AsyncSeriesHook(["compilation"]),

			/** @type {AsyncSeriesHook<Compiler>} */
			//监听模式下启动编译(常用于开发阶段)
			watchRun: new AsyncSeriesHook(["compiler"]),
			/** @type {SyncHook<Error>} */
			failed: new SyncHook(["error"]),
			/** @type {SyncHook<string, string>} */
			invalid: new SyncHook(["filename", "changeTime"]),
			/** @type {SyncHook} */
			// 如名字所述
			watchClose: new SyncHook([]),

			// TODO the following hooks are weirdly located here
			// TODO move them for webpack 5
			/** @type {SyncHook} */
			//初始化阶段:开始应用 Node.js 风格的文件系统到compiler 对象,以方便后续的文件寻找和读取。
			environment: new SyncHook([]),
			/** @type {SyncHook} */
			// 参照上文
			afterEnvironment: new SyncHook([]),
			/** @type {SyncHook<Compiler>} */
			// 调用完内置插件以及配置引入插件的apply方法,完成了事件订阅
			afterPlugins: new SyncHook(["compiler"]),
			/** @type {SyncHook<Compiler>} */
			afterResolvers: new SyncHook(["compiler"]),
			/** @type {SyncBailHook<string, EntryOptions>} */
			// 读取配置的 Entrys,为每个 Entry 实例化一个对应的 EntryPlugin,为后面该 Entry 的递归解析工作做准备。
			entryOption: new SyncBailHook(["context", "entry"])
		};

webpack 执行的过程中,会按顺序广播一系列事件--this.hooks中的一系列事件(类似于我们常用框架中的生命周期),而这些事件的订阅者该按照怎样的顺序来组织,来执行,来进行参数传递... 这就是 Tapable 要做的事情。
关于 Tapable 给大家推荐一篇比较好(但是阅读量点赞评论都不多2333)的科普文

流程细节

流程细节参照我在引用的Compile对象中的注释,有一点需要注意,作者hooks的书写顺序并不是调用顺序。 有些没注释的有几种情况:

  1. 不那么重要,或参照事件名称和上下文可知
  2. 主要是暂时还不知道(2333,后面有新的理解再补充,逃...)
  3. 当然最重要的事件基本涵盖到了 这里补充一个大从参考文章里面找来的图

compilation 过程简介

compilation 实际上就是调用相应的 loader 处理文件生成 chunks并对这些 chunks 做优化的过程。几个关键的事件(Compilation对象this.hooks中):

  1. buildModule 使用对应的 Loader 去转换一个模块;
  2. normalModuleLoader 在用 Loader 对一个模块转换完后,使用 acorn 解析转换后的内容,输出对应的抽象语法树(AST),以方便 webpack 后面对代码的分析。
  3. seal 所有模块及其依赖的模块都通过 Loader 转换完成后,根据依赖关系开始生成 Chunk

最后从参考文章中摘了一张图片以便于对整个过程有更清晰的认知

image

参考

  1. taobaofed.org/blog/2016/0…
  2. imweb.io/topic/5baca…
  3. 《深入浅出webpack》

广而告之

本文发布于薄荷前端周刊,欢迎Watch & Star ★,转载请注明出处。

欢迎讨论,点个赞再走吧 。◕‿◕。 ~