三者协作构成 webpack 的主体框架:
理解了这三块内容就算是入了个门,对 Webpack 有了一个最最基础的认知了,工作中再遇到问题也就能按图索骥了。补充一句,作为一份入门教程,本文不会展开太多 webpack 代码层面的细节 —— 我的精力也不允许,所以读者也不需要看到一堆文字就产生特别大的心理负担。
核心流程解析
======
首先,我们要理解一个点,Webpack 最核心的功能:
At its core, webpack is a static module bundler for modern JavaScript applications.
也就是将各种类型的资源,包括图片、css、js等,转译、组合、拼接、生成 JS 格式的 bundler 文件。官网首页的动画很形象地表达了这一点:
这个过程核心完成了 内容转换 + 资源合并 两种功能,实现上包含三个阶段:
-
初始化阶段:
-
- 初始化参数:从配置文件、 配置对象、Shell 参数中读取,与默认配置结合得出最终的参数
-
创建编译器对象:用上一步得到的参数创建
Compiler
对象 -
初始化编译环境:包括注入内置插件、注册各种模块工厂、初始化 RuleSet 集合、加载配置的插件等
-
开始编译:执行
compiler
对象的run
方法 -
确定入口:根据配置中的
entry
找出所有的入口文件,调用compilition.addEntry
将入口文件转换为dependence
对象 -
构建阶段:
-
- 编译模块(make):根据
entry
对应的dependence
创建module
对象,调用loader
将模块转译为标准 JS 内容,调用 JS 解释器将内容转换为 AST 对象,从中找出该模块依赖的模块,再 递归 本步骤直到所有入口依赖的文件都经过了本步骤的处理
- 编译模块(make):根据
-
完成模块编译:上一步递归处理所有能触达到的模块后,得到了每个模块被翻译后的内容以及它们之间的 依赖关系图
-
生成阶段:
-
- 输出资源(seal):根据入口和模块之间的依赖关系,组装成一个个包含多个模块的
Chunk
,再把每个Chunk
转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会
- 输出资源(seal):根据入口和模块之间的依赖关系,组装成一个个包含多个模块的
-
写入文件系统(emitAssets):在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统
单次构建过程自上而下按顺序执行,下面会展开聊聊细节,在此之前,对上述提及的各类技术名词不太熟悉的同学,可以先看看简介:
-
Entry
:编译入口,webpack 编译的起点 -
Compiler
:编译管理器,webpack 启动后会创建compiler
对象,该对象一直存活知道结束退出 -
Compilation
:单次编辑过程的管理器,比如watch = true
时,运行过程中只有一个compiler
但每次文件变更触发重新编译时,都会创建一个新的compilation
对象 -
Dependence
:依赖对象,webpack 基于该类型记录模块间依赖关系 -
Module
:webpack 内部所有资源都会以“module”对象形式存在,所有关于资源的操作、转译、合并都是以 “module” 为基本单位进行的 -
Chunk
:编译完成准备输出时,webpack 会将module
按特定的规则组织成一个一个的chunk
,这些chunk
某种程度上跟最终输出一一对应 -
Loader
:资源内容转换器,其实就是实现从内容 A 转换 B 的转换器 -
Plugin
:webpack构建过程中,会在特定的时机广播对应的事件,插件监听这些事件,在特定时间点介入编译过程
webpack 编译过程都是围绕着这些关键对象展开的,更详细完整的信息,可以参考 Webpack 知识图谱 。
初始化阶段
基本流程
学习一个项目的源码通常都是从入口开始看起,按图索骥慢慢摸索出套路的,所以先来看看 webpack 的初始化过程:
解释一下:
-
将
process.args + webpack.config.js
合并成用户配置 -
调用
validateSchema
校验配置 -
调用
getNormalizedWebpackOptions + applyWebpackOptionsBaseDefaults
合并出最终配置 -
创建
compiler
对象 -
遍历用户定义的
plugins
集合,执行插件的apply
方法 -
调用
new WebpackOptionsApply().process
方法,加载各种内置插件
主要逻辑集中在 WebpackOptionsApply
类,webpack 内置了数百个插件,这些插件并不需要我们手动配置,WebpackOptionsApply
会在初始化阶段根据配置内容动态注入对应的插件,包括:
-
注入
EntryOptionPlugin
插件,处理entry
配置 -
根据
devtool
值判断后续用那个插件处理sourcemap
,可选值:EvalSourceMapDevToolPlugin
、SourceMapDevToolPlugin
、EvalDevToolModulePlugin
-
注入
RuntimePlugin
,用于根据代码内容动态注入 webpack 运行时
到这里,compiler
实例就被创建出来了,相应的环境参数也预设好了,紧接着开始调用 compiler.compile
函数:
// 取自 webpack/lib/compiler.js
compile(callback) {
const params = this.newCompilationParams();
this.hooks.beforeCompile.callAsync(params, err => {
// …
const compilation = this.newCompilation(params);
this.hooks.make.callAsync(compilation, err => {
// …
this.hooks.finishMake.callAsync(compilation, err => {
// …
process.nextTick(() => {
compilation.finish(err => {
compilation.seal(err => {…});
});
});
});
});
});
}
Webpack 架构很灵活,但代价是牺牲了源码的直观性,比如说上面说的初始化流程,从创建 compiler
实例到调用 make
钩子,逻辑链路很长:
-
启动 webpack ,触发
lib/webpack.js
文件中createCompiler
方法 -
createCompiler
方法内部调用WebpackOptionsApply
插件 -
WebpackOptionsApply
定义在lib/WebpackOptionsApply.js
文件,内部根据entry
配置决定注入entry
相关的插件,包括:DllEntryPlugin
、DynamicEntryPlugin
、EntryPlugin
、PrefetchPlugin
、ProgressPlugin
、ContainerPlugin
-
Entry
相关插件,如lib/EntryPlugin.js
的EntryPlugin
监听compiler.make
钩子 -
lib/compiler.js
的compile
函数内调用this.hooks.make.callAsync
-
触发
EntryPlugin
的make
回调,在回调中执行compilation.addEntry
函数 -
compilation.addEntry
函数内部经过一坨与主流程无关的hook
之后,再调用handleModuleCreate
函数,正式开始构建内容
这个过程需要在 webpack 初始化的时候预埋下各种插件,经历 4 个文件,7次跳转才开始进入主题,前戏太足了,如果读者对 webpack 的概念、架构、组件没有足够了解时,源码阅读过程会很痛苦。
关于这个问题,我在文章最后总结了一些技巧和建议,有兴趣的可以滑到附录阅读模块。
构建阶段
基本流程
你有没有思考过这样的问题:
-
Webpack 编译过程会将源码解析为 AST 吗?webpack 与 babel 分别实现了什么?
-
Webpack 编译过程中,如何识别资源对其他资源的依赖?
-
相对于 grunt、gulp 等流式构建工具,为什么 webpack 会被认为是新一代的构建工具?
这些问题,基本上在构建阶段都能看出一些端倪。构建阶段从 entry
开始递归解析资源与资源的依赖,在 compilation
对象内逐步构建出 module
集合以及 module
之间的依赖关系,核心流程:
解释一下,构建阶段从入口文件开始:
-
调用
handleModuleCreate
,根据文件类型构建module
子类 -
调用 loader-runner 仓库的
runLoaders
转译module
内容,通常是从各类资源类型转译为 JavaScript 文本 -
调用 acorn 将 JS 文本解析为AST
-
遍历 AST,触发各种钩子
-
- 在
HarmonyExportDependencyParserPlugin
插件监听exportImportSpecifier
钩子,解读 JS 文本对应的资源依赖
- 在
-
调用
module
对象的addDependency
将依赖对象加入到module
依赖列表中 -
AST 遍历完毕后,调用
module.handleParseResult
处理模块依赖 -
对于
module
新增的依赖,调用handleModuleCreate
,控制流回到第一步 -
所有依赖都解析完毕后,构建阶段结束
这个过程中数据流 module => ast => dependences => module
,先转 AST 再从 AST 找依赖。这就要求 loaders
处理完的最后结果必须是可以被 acorn 处理的标准 JavaScript 语法,比如说对于图片,需要从图像二进制转换成类似于 export default "data:image/png;base64,xxx"
这类 base64 格式或者 export default "http://xxx"
这类 url 格式。
compilation
按这个流程递归处理,逐步解析出每个模块的内容以及 module
依赖关系,后续就可以根据这些内容打包输出。
示例:层级递进
假如有如下图所示的文件依赖树:
其中 index.js
为 entry
文件,依赖于 a/b 文件;a 依赖于 c/d 文件。初始化编译环境之后,EntryPlugin
根据 entry
配置找到 index.js
文件,调用 compilation.addEntry
函数触发构建流程,构建完毕后内部会生成这样的数据结构:
此时得到 module[index.js]
的内容以及对应的依赖对象 dependence[a.js]
、dependence[b.js]
。OK,这就得到下一步的线索:a.js、b.js,根据上面流程图的逻辑继续调用 module[index.js]
的 handleParseResult
函数,继续处理 a.js、b.js 文件,递归上述流程,进一步得到 a、b 模块:
从 a.js 模块中又解析到 c.js/d.js 依赖,于是再再继续调用 module[a.js]
的 handleParseResult
,再再递归上述流程:
到这里解析完所有模块后,发现没有更多新的依赖,就可以继续推进,进入下一步。
总结
回顾章节开始时提到的问题:
-
Webpack 编译过程会将源码解析为 AST 吗?webpack 与 babel 分别实现了什么?
-
- 构建阶段会读取源码,解析为 AST 集合。
-
Webpack 读出 AST 之后仅遍历 AST 集合;babel 则对源码做等价转换
-
Webpack 编译过程中,如何识别资源对其他资源的依赖?
-
- Webpack 遍历 AST 集合过程中,识别
require/ import
之类的导入语句,确定模块对其他资源的依赖关系
- Webpack 遍历 AST 集合过程中,识别
-
相对于 grant、gulp 等流式构建工具,为什么 webpack 会被认为是新一代的构建工具?
-
- Grant、Gulp 仅执行开发者预定义的任务流;而 webpack 则深入处理资源的内容,功能上更强大
生成阶段
基本流程
构建阶段围绕 module
展开,生成阶段则围绕 chunks
展开。经过构建阶段之后,webpack 得到足够的模块内容与模块关系信息,接下来开始生成最终资源了。代码层面,就是开始执行 compilation.seal
函数:
// 取自 webpack/lib/compiler.js
compile(callback) {
const params = this.newCompilationParams();
this.hooks.beforeCompile.callAsync(params, err => {
// …
const compilation = this.newCompilation(params);
this.hooks.make.callAsync(compilation, err => {
// …
this.hooks.finishMake.callAsync(compilation, err => {
// …
process.nextTick(() => {
compilation.finish(err => {
compilation.seal(err => {…});
});
});
});
});
});
}
seal
原意密封、上锁,我个人理解在 webpack 语境下接近于 “将模块装进蜜罐” 。seal
函数主要完成从 module
到 chunks
的转化,核心流程:
简单梳理一下:
-
构建本次编译的
ChunkGraph
对象; -
遍历
compilation.modules
集合,将module
按entry/动态引入
的规则分配给不同的Chunk
对象; -
compilation.modules
集合遍历完毕后,得到完整的chunks
集合对象,调用createXxxAssets
方法 -
createXxxAssets
遍历module/chunk
,调用compilation.emitAssets
方法将assets
信息记录到compilation.assets
对象中 -
触发
seal
回调,控制流回到compiler
对象
这一步的关键逻辑是将 module
按规则组织成 chunks
,webpack 内置的 chunk
封装规则比较简单:
-
entry
及 entry 触达到的模块,组合成一个chunk
-
使用动态引入语句引入的模块,各自组合成一个
chunk
chunk
是输出的基本单位,默认情况下这些 chunks
与最终输出的资源一一对应,那按上面的规则大致上可以推导出一个 entry
会对应打包出一个资源,而通过动态引入语句引入的模块,也对应会打包出相应的资源,我们来看个示例。
示例:多入口打包
假如有这样的配置:
const path = require(“path”);
module.exports = {
mode: “development”,
context: path.join(__dirname),
entry: {
a: “./src/index-a.js”,
b: “./src/index-b.js”,
},
output: {
filename: “[name].js”,
path: path.join(__dirname, “./dist”),
},
devtool: false,
target: “web”,
plugins: [],
};
实例配置中有两个入口,对应的文件结构:
index-a
依赖于c,且动态引入了 e;index-b
依赖于 c/d 。根据上面说的规则:
-
entry
及entry触达到的模块,组合成一个 chunk -
使用动态引入语句引入的模块,各自组合成一个 chunk
生成的 chunks
结构为:
也就是根据依赖关系,chunk[a]
包含了 index-a/c
两个模块;chunk[b]
包含了 c/index-b/d
三个模块;chunk[e-hash]
为动态引入 e
对应的 chunk。
不知道大家注意到没有,chunk[a]
与 chunk[b]
同时包含了 c,这个问题放到具体业务场景可能就是,一个多页面应用,所有页面都依赖于相同的基础库,那么这些所有页面对应的 entry
都会包含有基础库代码,这岂不浪费?为了解决这个问题,webpack 提供了一些插件如 CommonsChunkPlugin
、SplitChunksPlugin
,在基本规则之外进一步优化 chunks
结构。
SplitChunksPlugin
的作用
SplitChunksPlugin
是 webpack 架构高扩展的一个绝好的示例,我们上面说了 webpack 主流程里面是按 entry / 动态引入
两种情况组织 chunks
的,这必然会引发一些不必要的重复打包,webpack 通过插件的形式解决这个问题。
回顾 compilation.seal
函数的代码,大致上可以梳理成这么4个步骤:
-
遍历
compilation.modules
,记录下模块与chunk
关系 -
触发各种模块优化钩子,这一步优化的主要是模块依赖关系
-
遍历
module
构建 chunk 集合 -
触发各种优化钩子
上面 1-3 都是预处理 + chunks 默认规则的实现,不在我们讨论范围,这里重点关注第4个步骤触发的 optimizeChunks
钩子,这个时候已经跑完主流程的逻辑,得到 chunks
集合,SplitChunksPlugin
正是使用这个钩子,分析 chunks
集合的内容,按配置规则增加一些通用的 chunk :
module.exports = class SplitChunksPlugin {
constructor(options = {}) {
// …
}
_getCacheGroup(cacheGroupSource) {
// …
}
apply(compiler) {
// …
compiler.hooks.thisCompilation.tap(“SplitChunksPlugin”, (compilation) => {
// …
compilation.hooks.optimizeChunks.tap(
{
name: “SplitChunksPlugin”,
stage: STAGE_ADVANCED,
},
(chunks) => {
// …
}
);
});
}
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V获取:vip1024c (备注前端)
总结
=============================================================
从转行到现在,差不多两年的时间,虽不能和大佬相比,但也是学了很多东西。我个人在学习的过程中,习惯简单做做笔记,方便自己复习的时候能够快速理解,现在将自己的笔记分享出来,和大家共同学习。
个人将这段时间所学的知识,分为三个阶段:
第一阶段:HTML&CSS&JavaScript基础
第二阶段:移动端开发技术
第三阶段:前端常用框架
-
推荐学习方式:针对某个知识点,可以先简单过一下我的笔记,如果理解,那是最好,可以帮助快速解决问题;如果因为我的笔记太过简陋不理解,可以关注我以后我还会继续分享。
-
大厂的面试难在,针对一个基础知识点,比如JS的事件循环机制,不会上来就问概念,而是换个角度,从题目入手,看你是否真正掌握。所以对于概念的理解真的很重要。
如果你觉得这些内容对你有帮助,可以添加V获取:vip1024c (备注前端)*
[外链图片转存中…(img-mY4IG4Rp-1711803122734)]
总结
=============================================================
从转行到现在,差不多两年的时间,虽不能和大佬相比,但也是学了很多东西。我个人在学习的过程中,习惯简单做做笔记,方便自己复习的时候能够快速理解,现在将自己的笔记分享出来,和大家共同学习。
个人将这段时间所学的知识,分为三个阶段:
第一阶段:HTML&CSS&JavaScript基础
第二阶段:移动端开发技术
第三阶段:前端常用框架
-
推荐学习方式:针对某个知识点,可以先简单过一下我的笔记,如果理解,那是最好,可以帮助快速解决问题;如果因为我的笔记太过简陋不理解,可以关注我以后我还会继续分享。
-
大厂的面试难在,针对一个基础知识点,比如JS的事件循环机制,不会上来就问概念,而是换个角度,从题目入手,看你是否真正掌握。所以对于概念的理解真的很重要。