Webpack 本质上是一个静态模块打包器,它会从一个或多个入口文件开始,递归分析项目中所有用到的模块,将它们按照依赖关系组织成一颗依赖树,然后打包成一个(或多个)可在浏览器中运行的静态资源。下面拆解其核心过程。
打包流程:从入口到输出
Webpack 的一次完整打包可概括为三个阶段:
- 初始化阶段
读取配置文件(webpack.config.js),合并命令行参数与默认配置,验证并解析出最终配置对象。同时创建 Compiler 实例,加载所有内置及用户配置的插件,由插件在生命周期的不同钩子中注入处理逻辑。
- 编译构建阶段
从配置中的 entry 入口起点开始,调用对应的 Loader 将不同类型的文件(如 .less、.tsx、.png)转换为 Webpack 能够处理的 JavaScript 模块。每个文件作为一个模块,经过递归分析其引入的依赖(import / require / @import 等),逐步构建出一张完整的模块依赖图。
- 输出阶段
根据依赖图,Webpack 将处理后的模块组装成一个或多个 chunk(代码块),通过模板(如 MainTemplate、ChunkTemplate)将 chunk 转化为最终可以在浏览器端运行的 bundle 文件(例如 main.js、vendor.js)。最后将生成的文件写入 output.path 指定的磁盘目录,整个打包流程结束。
这个流程是高度可扩展的:Loader 在模块文件被加载时转换内容,Plugin 则能在打包的任意生命周期节点(如开始编译、生成资源、输出文件前)插入自定义行为。
模块解析:如何找到你 import 的那个文件
当 Webpack 在文件中遇到 import、require 等模块引入语句时,它需要准确找到目标文件在文件系统中的绝对路径,这一过程称为模块解析。
解析策略由 resolve 配置控制,主要包括:
- 绝对路径:直接使用,无需进一步解析。
- 相对路径:以当前文件所在目录为基准,拼接后查找。
- 模块路径(如
import Vue from 'vue'):在resolve.modules指定的目录(默认是node_modules)中按顺序查找。
对于每一个路径,Webpack 会按照 resolve.extensions 配置的后缀名顺序尝试补全文件名(如 .js、.jsx、.ts),并且支持为特定路径设置别名(resolve.alias),以简化引用路径。当找到的是一个目录时,Webpack 会尝试解析该目录下的 package.json 中 main 或 module 字段,或查找 index.js 文件。
模块解析过程的性能直接影响构建速度,因此在大型项目中,合理配置 resolve.extensions 优先匹配高频后缀、使用 resolve.alias 减少深层查找、指定绝对路径的 resolv.modules 等优化十分必要。
依赖图构建:编织模块之间的网
从入口模块开始,Webpack 会为每个文件创建一个 Module 对象,并分析其源代码,将其中引用的其他模块全部解析出来。这个递归收集的过程就像从树根向枝叶不断延伸,最终将所有用到的模块及其之间的依赖关系在内存中组织成一个模块依赖图。
具体过程:
- 读取入口文件,将其内容交给配置的 Loader 处理,得到转换后的 JavaScript 字符串。
- 将字符串解析为 AST(抽象语法树),借助
acorn等解析器分析代码,提取出所有的import、require、export语句,识别出该模块依赖哪些其他模块。 - 对每个依赖,按照解析规则找到对应的文件,然后递归对新文件重复上述过程,直到没有新的依赖被发现。
最终构建出的依赖图中,每个节点代表一个模块,边代表模块间的引用关系。Webpack 可以基于这张图进行优化,例如:
- 代码分割:将某些分支上的模块提取为独立 chunk,实现按需加载。
- Tree Shaking:标记未被导出的代码为 dead code,在输出时剔除。
- 依赖范围分析:避免重复打包同一个模块的多个副本。
依赖图的构建是 Webpack 打包能力的基础,也是实现各种高阶优化(缓存、分包、scope hoisting)的前提。正因为有了这张精确的模块关系网络,Webpack 才有能力将散落在项目中的上千个文件自动整理成一个个高效的可部署产出。