【揭秘Webpack】【1】构建中的Module、Chunk
写在前面
想要深入掌握 Webpack 构建机制,首先需要对其核心中间产物Module
和Chunk
以及由它们形成的依赖图有一个清晰的认识。这些构建块不仅代表了 Webpack 工作流的心脏,也是理解其精妙构建逻辑的基础。通过了解这些中间产物及其相互关系,我们才能明白构建过程中各种行为背后的原因。
Module 和 Chunk 是什么?
在Webpack世界里,Module
是代码的最基本单元,代表着一个个独立的文件或模块,JS文件、CSS文件或者其他任何类型的资源。Webpack 通过分析代码中的 import
和 require
语句来识别这些 Module
,并构建出一个 Module Graph
,明确哪些模块彼此依赖。
而 Chunk
则是在Webpack的打包过程中形成的,它是一组有共同依赖关系的 Module
的集合。通常一个Chunk
对应了最终输出的一个 bundle
文件,这个文件中可能包含了一个或多个模块。
总而言之,Module
负责描述单个代码块和资源的细节,而 Chunk
则确定如何有效地加载和组织这些代码和资源。
为了方便理解,下图作为简化的示意图,实际的构建过程会更为复杂
调试小技巧: 可以通过
compilation.modules
、compilation.chunks
或者compilation
上的hooks
回调函数中的参数来读取对应的数据。然后通过不同生命周期的hooks,可以观察到module
和chunk
在不同阶段的变化。示例如下:
class MyPlugin {
// module 和 chunk 在编译过程中会不断被加工,不同阶段下的输出可能会存在差异
apply(compiler) {
compiler.hooks.compilation.tap("MyPlugin", compilation => {
compilation.hooks.succeedModule.tap("MyPlugin", (module) => {
console.log('succeedModule hooks module', module);
});
compilation.hooks.optimizeChunks.tap('ChunkListPlugin', chunks => {
chunks.forEach(chunk => {
console.log('optimizeChunks hooks chunk', chunk);
});
});
});
compiler.hooks.afterCompile.tap("MyPlugin", compilation => {
compilation.modules.forEach(module => {
console.log('compilation.modules', module);
});
compilation.chunks.forEach(chunk => {
console.log('compilation.chunks', chunk);
});
});
}
}
Module 解析
在Webpack中,模块是指是对项目中构建块(单个文件或资源)的抽象。由于需要针对不同类型的资源和处理逻辑,Webpack 因此设计了多个模块类型来专门处理这些差异,但这些模块类型都派生自一个共同的基类——Module
。Module
定义了所有模块共有的属性和方法,确保所有模块在核心功能上保持统一。
Module 基类
职责
- 存储模块的基本数据(例如,模块ID、依赖关系等)。
- 描述模块对其它模块的依赖方式。
- 为构建过程中模块的加载、打包和更新提供接口。
- 提供序列化和反序列化的方法,以支持缓存和异步加载。
核心属性
下面列举一些核心属性的作用。便于后续的分析理解
属性 | 作用 |
---|---|
debugId | 内部使用的模块标识符,主要用于调试,帮助开发者跟踪模块在构建中的处理过程。通常从1000开始累加,也可通过该值判断module 创建的顺序 |
dependencies | 包含模块的同步依赖列表,表示文件间直接的关联和需要立即加载的资源。比如代码中直接的import ,初始化时立即加载 |
block | 与dependencies类似,但主要用于处理如异步import() 生成的代码分割和按需加载的依赖,在运行时按需加载,不会立即初始化 |
issuer | 指向引入当前模块的那个模块,通常会是第一个import 该模块的那个模块 |
identifier() | 返回表示模块唯一性的字符串,用于模块缓存和追踪。 |
size() | 返回模块的大小,通常是指编译后的源码大小 |
build() | 负责编译模块,处理依赖关系,并生成最终的可执行代码 |
source() | 返回模块的原始或转换后的源代码,作为构建输出的基础 |
addChunk(chunk)、removeChunk(chunk)、isInChunk(chunk)、getChunks()、... | 与 chunk 交互的增删改查方法 |
serialize() / deserialize() | 模块的序列化和反序列化,以便持久化或网络传输 |
Module 子类
作为开发者,在日常的开发中大多数时候是和与Module
的子类打交道。Module
的子类是根据不同类型的文件和资源来进行差异化的处理。
下面将简要介绍一些常用的 Module
子类,主要想表达不同子类在作用上的差异。后续文章会对NormalModule
进行深入解析,其余子类的深入可自行探索
- NormalModule: 用于处理各种类型的文件,通过 loaders 对文件内容进行转换,并生成最终的模块代码。是最常规的模块
- ContextModule: 处理动态请求(如
require.context
)生成的依赖关系,允许你引入一个目录下的所有模块。通常用在不确定具体需要哪个模块的场景。 - MultiModule: 当你使用
require.ensure()
或动态import()
来加载多个依赖时使用。它将多个子模块合并为一个模块,以支持对其统一管理。 - ExternalModule: 用于引入外部的全局依赖,这些依赖在运行时从环境中获取而不是包含在 webpack 的输出 bundle 中。提高了构建性能,因为不需要打包和处理指定为外部的库。
- CssModule: 专门用于处理 CSS 文件,将 CSS 文件转换为 JavaScript 模块。 与 CSS 相关的 loader (如
css-loader
、style-loader
)一起工作,允许将CSS应用于DOM或导出为独立文件。 - RuntimeModule: 包含 webpack 运行时代码的模块,为模块的加载和解析提供额外的逻辑。通常专用于 webpack 的运行时加载器和模块解析逻辑。
Chunk 解析
chunk
是编译过程的核心单元,以多个模块module
的聚合体形式存在,通常会对应生成的一个或多个输出文件。chunk
通常源自入口点配置(由entry
定义)或者由动态导入(如import()
)实现的代码分割。此外,它们也可以由splitChunks
插件进行更细致的划分。chunk
的设计思路与module
不同,主要是module
的集合而不是派生种类,因此它们通过一组属性来识别类型和作用,而非子类。
职责
- 模块聚合: 为若干逻辑相关的
module
的集合。在构建过程中处理module
的一个更高级别的抽象。 - 代码拆分: 通过Chunks实现应用的按需加载或分割,加快首次加载速度,改善体验。
- 生成输出文件: 构建最终产物是输出文件,每个
chunk
负责输出一组文件。 - 运行时管理: 包括了启动应用程序或代码拆分块所需的运行时代码,确保模块正确加载和解析依赖。
- 依赖优化: 运用Tree Shaking等技术优化去除多余代码,减少输出体积。
- 代码复用: 识别并抽离共享模块,创建独立的Chunk来减少重复代码。
- 利用缓存: 利用文件哈希值命名,实现更高效的浏览器缓存和更新机制。
- 模块热替换: 实现HMR,即在不刷新网页的情况下替换、添加或删除模块。
核心属性
下面列举一些核心属性的作用。便于后续的分析理解
属性 | 作用 |
---|---|
debugId | 内部使用的模块标识符,主要用于调试。通常从1000开始累加,也可通过该值判断chunk 创建的顺序 |
id | 唯一标识符,经常用于引用和跟踪chunk |
ids | 在模块热替换(HMR)等特定场景下,一个chunk 可能需要多个id ,这就是ids 数组出现的意义,它包含了那个chunk 所有的id 。在普通构建过程中,ids 往往只有一个,等于id |
modulesSize() | 通常表示chunk 中所有模块原始代码的合计大小,不包括Webpack运行时的代码或者其他引导模板代码。 |
size() | 整个chunk 构建好后的总大小,包含了modulesSize 的大小。该属性对优化决策很重要。 |
getModules()、modulesIterable | 获取它所包含的模块的信息,getModules() 的是数组,modulesIterable 为可迭代对象 |
integrate(otherChunk) | 合并两个chunks |
canBeInitial() | 判断一个chunk 是否可以作为起始加载点。返回 true ,这表明chunk 可以作为初始块来使用;返回 false ,表示不适合作为入口点使用,可能是异步加载的代码块,或者是通过代码分割分离出来的,只能被其他 chunk 引用或按需加载。 |
moveModule(module, otherChunk)、addModule(module)、removeModule(module)、containsModule(module) | 与 module 交互的增删改查方法 |
总结与下期预告
本文浅谈了module
和chunk
:它们是什么,长什么样。希望大家对这两个概念有个大概了解。关于它们如何转换和互相影响,以及能怎么对它们进行优化,这些内容会留在了下篇文章。下次我们将深度解析moduleGraph
和chunkGraph
,带大家更深入地掌握这些概念。
敬请关注,感兴趣的同学可订阅文章结尾处专栏!
转载自:https://juejin.cn/post/7373505141489958923