摘要
本文阐述了 Webpack 在前端开发工具链中的重要性,剖析其结构与源码,包括核心流程、重要对象以及插件和加载器的工作原理,还总结了相关常见问题。
一、Webpack 的重要性及学习必要性
Webpack 在现代前端开发工具链中地位不可替代,具有优秀的灵活性和强大的生态系统。然而随着版本更新,其功能增多、代码量庞大,学习难度提高。很多人对 Webpack 的使用仅停留在配置层面,这会导致在实现功能时不清楚原理,面试时被追问就难以作答等问题。所以深入学习 Webpack 的底层原理和架构设计非常必要。本文将从 JS 打包核心流程、Plugin 的作用与原理、Loader 的作用与原理这三方面构建读者对 Webpack 的完整认知体系,且不会涉及代码分割、按需加载等功能实现。文中涉及大量源码,已做压缩处理并添加注释方便阅读。
二、Webpack 的基本概念
(一)入口(Entry)
Entry
是构建的起点,Webpack 从这里开始执行构建,通过Entry
配置能确定开始构建的文件,进而识别出应用程序的依赖图谱。例如在webpack.config.js
中配置entry: './src/index.js'
,Webpack 就会从index.js
开始分析依赖关系。
// 示例配置
module.exports = {
entry: './src/index.js'
};
(二)模块(Module)
在 Webpack 视角下,一切文件皆可视为模块,像 JavaScript、CSS、图片等类型的文件都是。Webpack 从Entry
出发,递归地构建出包含所有依赖文件的模块网络。
(三)代码块(Chunk)
Chunk
是代码的集合体,由模块合并而成,可优化输出文件的结构,支持代码的懒加载、拆分等高级功能。
(四)加载器(Loader)
Loader
是模块的转换器,由于 Webpack 本身只理解 JavaScript,Loader
使 Webpack 能够处理非 JavaScript 文件。例如将 CSS 转换为 JS 模块或者将高版本 JavaScript 转换为兼容性更好的形式。
// 示例,使用 babel - loader 处理 JavaScript 文件
module.exports = {
module: {
rules: [
{
test: /.js$/,
use: 'babel - loader'
}
]
}
};
(五)插件(Plugin)
Plugin
是构建流程的参与者,Webpack 的构建流程中有很多事件钩子(hooks),Plugin
可监听这些事件触发,在触发时加入自定义构建行为,如自动压缩打包后的文件、生成 HTML 文件等。
三、JS 打包的核心流程
(一)Webpack 执行入口
Webpack 的执行入口在./lib/webpack.js
,核心函数接收webpack
配置和可选的回调函数。其中compiler.run(callback)
的执行正式开启了 Webpack 的编译过程。
// 核心函数部分代码
const webpack = (
// 接收 webpack 配置和可选的回调函数
(options, callback) => {
// 根据配置创建编译器的简化版函数
const create = () => {
let compiler;
let watch = false;
let watchOptions;
return { compiler, watch, watchOptions };
};
const { compiler, watch, watchOptions } = create();
if (watch) {
compiler.watch(watchOptions, callback);
} else if (callback) {
compiler.run(callback);
}
return compiler;
}
);
(二)compiler 和 compilation
- Compiler
- 是 Webpack 的核心,贯穿整个构建周期,封装了 Webpack 环境的全局配置,如配置信息、输出路径等。
compiler
的创建过程在createCompiler
函数中,这个函数标准化配置、应用默认值、创建Compiler
实例、注册插件、触发相关钩子等操作。
- Compilation
- 表示单次的构建过程及其产出,每次构建都会新创建,描述构建的具体过程,包括模块资源、编译后的产出资源、文件变化和依赖关系状态等。在
watch mode
下,文件变化触发重新构建时会生成新的Compilation
实例。
- 表示单次的构建过程及其产出,每次构建都会新创建,描述构建的具体过程,包括模块资源、编译后的产出资源、文件变化和依赖关系状态等。在
(三)编译阶段
- compiler.run()的实现
- 在
./lib/Compiler.js
中的compiler.run()
函数实现里,触发了很多钩子,如beforeRun
、run
、afterDone
等,核心是run
周期中的回调函数this.compile(onCompiled)
。
- 在
- compiler.compile()
- 在
compile
函数中,按照beforeCompile - compile - make - finishMake - afterCompile
(不完全,还有seal
等)的钩子执行顺序进行操作。其中make
阶段实现了整个编译过程,相关编译逻辑在EntryPlugin
中,而EntryPlugin
在EntryOptionPlugin
中被实例化,在WebpackOptionsApply
中被引入,在compiler.run()
之前就被注册好了。
- 在
- 添加 Entry
- 在
EntryPlugin
中的compilation.addEntry
主要处理Entry
,添加过程中会调用addModuleTree()
,依据代码依赖关系递归构建模块树。
- 在
- 添加 Module
- 涉及
addModuleTree()
及后续进程,是生成Module
的过程。其中addModule
函数将模块添加到编译过程,构建内容在addModule
回调中的_handleModuleBuildAndDependencies()
中执行。在buildModule
函数里,会调用构建模块钩子、实际进行模块构建、将构建后的模块存储到缓存等操作。并且build
方法存在继承问题,在./lib/NormalModule.js
的build
方法中还有_doBuild()
,这里会调用加载器之前的钩子、执行加载器处理流程等。
- 涉及
四、Loader 处理
- loader - runner 中的处理逻辑
- 从
loader - runner
包中导入runLoaders
作为loader
的处理函数。在loader - runner
中,通过迭代处理每一个loader
,在loadLoader
中检验并加载好loader
后,在其回调中执行loader
的pitch
方法完成loader
处理。
- 从
五、Parse 处理
- 解析模块依赖关系
Parse
主要是对模块代码进行解析,构建出抽象语法树(AST)来描述模块依赖关系。Webpack 会分析代码中的import
、require
等语句找出依赖关系构建模块依赖图,这对后续优化操作很重要。Parse
的主要作用是处理模块间依赖关系并将关系数据存储在module.dependencies
数组中。- 借助第三方库
acorn
实现AST
转换,在this.parser.parse
函数中完成相关操作。
六、打包封装模块
(一)封装 Chunk
- seal 的核心处理过程
- 在
compilation.seal()
中有chunk
处理逻辑,其核心处理过程包括创建ChunkGraph
、遍历入口点、构建Chunk Graph
、生成Chunk Assets
等步骤。 - 关于
chunk
类型,有Entry Chunks
(每个入口点至少生成一个entry chunk
,确保入口有对应的chunk
包含启动代码,通过entry
配置指定)和Async Chunks
(使用import()
语句导入的模块会被封装到新的async chunk
中,实现代码拆分和懒加载)。
- 在
(二)通过 emit 将 Assets 输出
- createChunkAssets 中的逻辑
- 在
createChunkAssets
函数中,核心处理在render
和emitAsset
中,render
触发对不同资源的处理打出最终资源内容Source
,Source
被传入emitAsset
用来将生成的资源添加到最终输出部分。而最终的输出在compiler.emitAssets
中触发,compiler.emitAssets
在compiler.run
的onCompiled
中被调用,它会将资源写入文件系统。
- 在
七、常见问题总结
(一)Asset 和 Bundle 的区别
- Bundle
- 主要是 JavaScript 文件,也可包含其他类型文件(如通过插件或
loader
生成的 CSS、HTML)。
- 主要是 JavaScript 文件,也可包含其他类型文件(如通过插件或
- Asset
- 指构建过程中生成的任何类型的文件,包括
Bundle
本身和其他资源(如图片、字体、样式表等)。
- 指构建过程中生成的任何类型的文件,包括
(二)如何手写 Webpack 插件
- 需要进一步了解
Tapable
的内容,可以参考相关文章学习。
(三)构建工具的横向对比
- 通过表格对比流行的构建工具,创建项目时要综合需求选择。
(四)阅读源码的方法
- 调整状态、心态
- 要有自驱力,明确读源码的目的;保持好奇心和探究心,对不懂的部分积极探索;要有耐力,耐心分析代码;保持安静,专注阅读。
- 掌握工具
- 可使用
debugger
调试源码、查阅文档、结合 AI 精简源码、进行文档记录等,还可根据目标源码特点协助阅读,如 Webpack 可先从整体了解其hooks
机制。
- 可使用