likes
comments
collection
share

从数据结构的角度看 Webpack

作者站长头像
站长
· 阅读数 14

前言

随着 Webpack 成为这几年前端工程化顶梁柱般的存在,作为一个前端工程师,可能对于 ModuleChunk等概念耳熟能详,但是可能对于 DependencyEntryDependency等概念就有点模棱两可。虽然大家对常见的 Module有比较直观的理解:对于 Webpack 来说,一切资源都是 Module(比如包括 JS、CSS、Image 等)。但是如果要深入理解这些概念,就需要从 Webpack 源码数据结构设计的角度来看,它们分别代表了 Webpack 构建过程中不同阶段的核心数据结构抽象。

本文将从 Webpack 源码数据结构设计地角度带大家更加深入了解:DependencyModuleChunk等概念,在此之前,我们把 Webpack 构建划分为三个阶段:初始化阶段、构建阶段、生成阶段

Dependency

Webpack 源码中,Dependency初始化阶段最基本的数据结构,对应的类维护在 Webpack 源码目录下的lib/Dependency.js

class Dependency {
	constructor() {
		/** @type {Module} */
		this._parentModule = undefined;
		/** @type {DependenciesBlock} */
		this._parentDependenciesBlock = undefined;
    // 省略一些代码
		this._locSL = 0;
		this._locSC = 0;
		this._locEL = 0;
		this._locEC = 0;
		this._locI = undefined;
		this._locN = undefined;
		this._loc = undefined;
	}
	/**
	 * @returns {DependencyLocation} location
	 */
	get loc() {
		// 省略一些代码
	}

	set loc(loc) {
		// 省略一些代码
	}

	setLoc(startLine, startColumn, endLine, endColumn) {
		// 省略一些代码
	}
  // 省略一些代码
}

module.exports = Dependency;

Dependency中维护了代码的 loc信息、_parentModule(父模块)等信息,loc代表的 Node 节点在 AST 中的位置信息。想了解更多,可以看 estree规范:node_objects

Webpack 初始化阶段就是从 entry 配置开始分析,将 entry 对应的文件以及所有 entry 引用的模块(文件)通过 compilation.addEntry 方法转换成 Dependency的过程 。当然 ,Dependency 只是最基础的数据结构,在 Webpack 源码中,存在以下基于 Dependency扩展的类:

  • ModuleDependency,作为模块 Dependency的基类,EntryDependency就是继承此类进行扩展;
  • EntryDependency ,初始化阶段,在EntryPlugin中就会根据 entry配置创建 EntryDependency (虽然该类不是直接继承 Dependecy,但是核心的状态和方法都来自 Dependecy );
  • ContainerEntryDependency,专门用于 ModuleFederation插件,根据插件的exposesname等配置会生成该类的实例;

除此之外,还有 ContextDependency、FallbackDependency、DllEntryDependency、NullDependency、LazyCompilationDependency、ProvideSharedDependency 等类,这里就不一一介绍,感兴趣的小伙伴可以自行去看相关的源码。Webpack 真正在使用的时候是以 Dependency 作为基类创建的以上的这些类,很少直接使用 Dependency

Module

有了 Denpendency,代表初始化阶段就完成了,接下来就进入构建阶段:那就是将 entry 对应生成的 Dependencies 创建生成 Module,在 Webpack 源码也有相对应的数据结构,ModuleWebpack 构建时处理的最小单位。跟 Dependency类似,Module也不是直接使用的类,甚至它都不是模块最基础的类,Module继承自 DependenciesBlock:

class DependenciesBlock {
	constructor() {
		/** @type {Dependency[]} */
		this.dependencies = [];
		/** @type {AsyncDependenciesBlock[]} */
		this.blocks = [];
		/** @type {DependenciesBlock} */
		this.parent = undefined;
	}
  
  // 省略一些源码

	/**
	 * @param {Dependency} dependency dependency being tied to block.
	 * This is an "edge" pointing to another "node" on module graph.
	 * @returns {void}
	 */
	addDependency(dependency) {
		this.dependencies.push(dependency);
	}

	/**
	 * @param {Dependency} dependency dependency being removed
	 * @returns {void}
	 */
	removeDependency(dependency) {
		const idx = this.dependencies.indexOf(dependency);
		if (idx >= 0) {
			this.dependencies.splice(idx, 1);
		}
	}

	/**
	 * Removes all dependencies and blocks
	 * @returns {void}
	 */
	clearDependenciesAndBlocks() {
		this.dependencies.length = 0;
		this.blocks.length = 0;
	}
}

module.exports = DependenciesBlock;

可以发现一个 Module比较核心的状态 dependencies和方法 addDependency是在 DependenciesBlock 中维护的。然后 Module再继承该类,而在源码中比较常用的 NormalModule再继承自 Module。限于 Module的源码比较多,这里就不一一贴太多的源码,对于没有看过源码的同学肯定看完是懵逼的状态。这里我直接上一个构建阶段的流程图:

从数据结构的角度看 Webpack

从图中,我们就非常清楚的了解到 Module在构建流程中的核心作用,其实就是调用 build方法,然后调用 runLoaders 方法使用配置中的各loader 将各种文件转成标准的 JS 模块(一般情况下是 es module),最后调用 acorn 这样的库将 JS 源码转成 AST,这也是为什么需要 loader 将一些非标准 JS 的文件转成标准 JS 模块的原因。有了 AST 就好办了,接着就可以分析 AST 生成各模块依赖关系,这个过程就生成了 Webapck 构建流程中非常核心的模块依赖图,所以在 Webpack 源码中也会有 ModuleGraph 这样的数据结构,用来存储这个阶段生成的模块和各模块之间的关系实际上 Webpack 构建阶段就是生成 Module 和模块依赖图的过程

在 Webpack 源码中,除了一般模块的继承自 ModuleNormalModule,还有以下模块类:

  • RuntimeModule,Webpack 运行时代码对应的模块,在构建产物中不可缺少的模块,可以通过 entry 或者 SplitChunkPlugins 的配置单独为该模块生成一个 Chunk;
  • ContainerEntryModule,专门用于 ModuleFederation插件,根据插件的exposesname等配生成各种 mf 相关的模块;
  • ExternalModule,根据 external配置生成的模块,专门处理 external相关的功能。

除此之外,还有 ContextModule、DelegatedModule、DllModule、LazyCompilationProxyModule、RawModule、RawDataUrlModule、FallbackModule、RemoteModule、ConcatenatedModule、ConsumeSharedModule 等模块,这样感兴趣的小伙也可以自行去看相关源码。

Chunk

Chunk 是生成文件之前最后一个数据结构的抽象,它是在最后生成阶段的时候”组装“出来的。很容易理解,实际上在编译阶段生成的所有 Module,在生成阶段就会根据模块之间的关系将一个或者多个 Module 组装起来生成一个个 Chunk。在 Webpack 默认的策略中,以下场景会默认生成一个 Chunk

  • entry 配置,一个 entry 以及文件所引用的所有 JS 模块组合成一个 Chunk
  • import 一个文件会单独生成一个 Chunk

如果对 Webpack 有理解的人可能会马上意识到上面生成 Chunk的 策略有很大的弊端,如果只有上述两种场景中生成 Chunk,那在单页面应用这种场景中,一般只有一个入口,这样生成的 Chunk 岂不是体积很大?怎么解决?

有了 Chunk 就简单了,在生成完所有的 Chunk 后,实际上 Webpack 会将构建控制权交回给 Compiler 对象,然后它根据用户的配置生成对应的文件,正常情况下,一个 Chunk 对应一个文件,这些文件也就是我们口中常说的 Bundle 了。实际上,我理解 Bundle 就是 Webpack 构建产物的一种更加书面的说法,通俗的讲,它表示的就是构建后生成的 js、css、image 等文件。

总结

对于 DependencyModuleChunk,实际上,个人理解,它们代表的就是 Webpack 构建过程中不同阶段核心数据结构的抽象,这些名词不是随便造出来的概念,实际在 Webpack 源码中有相应的数据结构对应。从 Webpack 构建流程看,它们存在一个线性的关系: Dependency(初始化阶段) -> Module(构建阶段) -> Chunk(生成阶段)

Webpack 作为一个扩展性极强的工具,我个人理解不只是体现在它的基于 tapable 的插件化机制下,虽然 hook 机制让我们非常容易在构建的各个阶段对 Webpack 功能进行一些扩展,但是一些从最早期就已经存在的数据结构也同样进一步曾强了 Webpack 的扩展能力。下面是一张 Webpack 比较早期的时候就存在的 UML 图:

从数据结构的角度看 Webpack

我们发现对于核心的几个数据结构,在 Webpack 早起的源码中就已经存在。Webpack 经历过几个大版本,都还是围绕着 DependencyModuleChunk在运转。实际上 Webpack5中新增的 Module Federation 机制,也同样是跟一般构建流程中的 EntryDependencyNormalModule等数据结构核心链路走的是一样的,只不过在 MF 入口构建流程中,对应的数据结构是 ContainerEntryDependencyContainerEntryModule。包括今年出现的 lazyCompilation 功能,再次验证了 Webpack 数据结构扩展性的强大。

Reference

最后打一个广告,本人最近也创建了自己的公众号,不定时的会更新前端技术文章和读书感悟,有兴趣的小伙伴可以加个关注:

从数据结构的角度看 Webpack