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) => {
// …
}
);
});
}
};
理解了吗?webpack 插件架构的高扩展性,使得整个编译的主流程是可以固化下来的,分支逻辑和细节需求“外包”出去由第三方实现,这套规则架设起了庞大的 webpack 生态,关于插件架构的更多细节,下面 plugin
部分有详细介绍,这里先跳过。
写入文件系统
经过构建阶段后,compilation
会获知资源模块的内容与依赖关系,也就知道“输入”是什么;而经过 seal
阶段处理后, compilation
则获知资源输出的图谱,也就是知道怎么“输出”:哪些模块跟那些模块“绑定”在一起输出到哪里。seal
后大致的数据结构:
compilation = {
// …
modules: [
/* … */
],
chunks: [
{
id: “entry name”,
files: [“output file name”],
hash: “xxx”,
runtime: “xxx”,
entryPoint: {xxx}
// …
},
// …
],
};
seal
结束之后,紧接着调用 compiler.emitAssets
函数,函数内部调用 compiler.outputFileSystem.writeFile
方法将 assets
集合写入文件系统,实现逻辑比较曲折,但是与主流程没有太多关系,所以这里就不展开讲了。
资源形态流转
OK,上面已经把逻辑层面的构造主流程梳理完了,这里结合资源形态流转的角度重新考察整个过程,加深理解:
-
compiler.make
阶段: -
entry
文件以dependence
对象形式加入compilation
的依赖列表,dependence
对象记录有entry
的类型、路径等信息
-
根据
dependence
调用对应的工厂函数创建module
对象,之后读入module
对应的文件内容,调用loader-runner
对内容做转化,转化结果若有其它依赖则继续读入依赖资源,重复此过程直到所有依赖均被转化为module
-
compilation.seal
阶段: -
- 遍历
module
集合,根据entry
配置及引入资源的方式,将module
分配到不同的chunk
- 遍历
-
遍历
chunk
集合,调用compilation.emitAsset
方法标记chunk
的输出规则,即转化为assets
集合 -
compiler.emitAssets
阶段: -
- 将
assets
写入文件系统
- 将
Plugin 解析
=========
网上不少资料将 webpack 的插件架构归类为“事件/订阅”模式,我认为这种归纳有失偏颇。订阅模式是一种松耦合架构,发布器只是在特定时机发布事件消息,订阅者并不或者很少与事件直接发生交互,举例来说,我们平常在使用 HTML 事件的时候很多时候只是在这个时机触发业务逻辑,很少调用上下文操作。而 webpack 的钩子体系是一种强耦合架构,它在特定时机触发钩子时会附带上足够的上下文信息,插件定义的钩子回调中,能也只能与这些上下文背后的数据结构、接口交互产生 side effect,进而影响到编译状态和后续流程。
学习插件架构,需要理解三个关键问题:
-
WHAT: 什么是插件
-
WHEN: 什么时间点会有什么钩子被触发
-
HOW: 在钩子回调中,如何影响编译状态
What: 什么是插件
从形态上看,插件通常是一个带有 apply
函数的类:
class SomePlugin {
apply(compiler) {
}
}
apply
函数运行时会得到参数 compiler
,以此为起点可以调用 hook
对象注册各种钩子回调,例如:compiler.hooks.make.tapAsync
,这里面 make
是钩子名称,tapAsync
定义了钩子的调用方式,webpack 的插件架构基于这种模式构建而成,插件开发者可以使用这种模式在钩子回调中,插入特定代码。webpack 各种内置对象都带有 hooks
属性,比如 compilation
对象:
class SomePlugin {
apply(compiler) {
compiler.hooks.thisCompilation.tap(‘SomePlugin’, (compilation) => {
compilation.hooks.optimizeChunkAssets.tapAsync(‘SomePlugin’, ()=>{});
})
}
}
钩子的核心逻辑定义在 Tapable 仓库,内部定义了如下类型的钩子:
const {
SyncHook,
SyncBailHook,
SyncWaterfallHook,
SyncLoopHook,
AsyncParallelHook,
AsyncParallelBailHook,
AsyncSeriesHook,
AsyncSeriesBailHook,
AsyncSeriesWaterfallHook
} = require(“tapable”);
不同类型的钩子根据其并行度、熔断方式、同步异步,调用方式会略有不同,插件开发者需要根据这些的特性,编写不同的交互逻辑,这部分内容也特别多,回头展开聊聊。
When: 什么时候会触发钩子
了解 webpack 插件的基本形态之后,接下来需要弄清楚一个问题:webpack 会在什么时间节点触发什么钩子?这一块我认为是知识量最大的一部分,毕竟源码里面有237个钩子,但官网只介绍了不到100个,且官网对每个钩子的说明都太简短,就我个人而言看完并没有太大收获,所以有必要展开聊一下这个话题。先看几个例子:
-
compiler.hooks.compilation
: -
- 时机:启动编译创建出 compilation 对象后触发
-
参数:当前编译的 compilation 对象
-
示例:很多插件基于此事件获取 compilation 实例
-
compiler.hooks.make
: -
- 时机:正式开始编译时触发
-
参数:同样是当前编译的
compilation
对象 -
示例:webpack 内置的
EntryPlugin
基于此钩子实现entry
模块的初始化 -
compilation.hooks.optimizeChunks
: -
- 时机:
seal
函数中,chunk
集合构建完毕后触发
- 时机:
-
参数:
chunks
集合与chunkGroups
集合 -
示例:
SplitChunksPlugin
插件基于此钩子实现chunk
拆分优化 -
compiler.hooks.done
: -
- 时机:编译完成后触发
-
参数:
stats
对象,包含编译过程中的各类统计信息 -
示例:
webpack-bundle-analyzer
插件基于此钩子实现打包分析
这是我总结的钩子的三个学习要素:触发时机、传递参数、示例代码。
触发时机
触发时机与 webpack 工作过程紧密相关,大体上从启动到结束,compiler
对象逐次触发如下钩子:
而 compilation
对象逐次触发:
所以,理解清楚前面说的 webpack 工作的主流程,基本上就可以捋清楚“什么时候会触发什么钩子”。
参数
传递参数与具体的钩子强相关,官网对这方面没有做出进一步解释,我的做法是直接在源码里面搜索调用语句,例如对于 compilation.hooks.optimizeTree
,可以在 webpack 源码中搜索 hooks.optimizeTree.call
关键字,就可以找到调用代码:
// lib/compilation.js#2297
this.hooks.optimizeTree.callAsync(this.chunks, this.modules, err => {
});
结合代码所在的上下文,可以判断出此时传递的是经过优化的 chunks
及 modules
集合。
找到示例
Webpack 的钩子复杂程度不一,我认为最好的学习方法还是带着目的去查询其他插件中如何使用这些钩子。例如,在 compilation.seal
函数内部有 optimizeModules
和 afterOptimizeModules
这一对看起来很对偶的钩子,optimizeModules
从字面上可以理解为用于优化已经编译出的 modules
,那 afterOptimizeModules
呢?
从 webpack 源码中唯一搜索到的用途是 ProgressPlugin
,大体上逻辑如下:
compilation.hooks.afterOptimizeModules.intercept({
name: “ProgressPlugin”,
call() {
handler(percentage, “sealing”, title);
},
done() {
progressReporters.set(compiler, undefined);
handler(percentage, “sealing”, title);
},
result() {
handler(percentage, “sealing”, title);
},
error() {
handler(percentage, “sealing”, title);
},
tap(tap) {
// p is percentage from 0 to 1
// args is any number of messages in a hierarchical matter
progressReporters.set(compilation.compiler, (p, …args) => {
handler(percentage, “sealing”, title, tap.name, …args);
});
handler(percentage, “sealing”, title, tap.name);
}
});
基本上可以猜测出,afterOptimizeModules
的设计初衷就是用于通知优化行为的结束。
apply
虽然是一个函数,但是从设计上就只有输入,webpack 不 care 输出,所以在插件中只能通过调用类型实体的各种方法来或者更改实体的配置信息,变更编译行为。例如:
-
compilation.addModule :添加模块,可以在原有的 module 构建规则之外,添加自定义模块
-
compilation.emitAsset:直译是“提交资产”,功能可以理解将内容写入到特定路径
到这里,插件的工作机理和写法已经有一个很粗浅的介绍了,回头单拎出来细讲吧。
How: 如何影响编译状态
解决上述两个问题之后,我们就能理解“如何将特定逻辑插入 webpack 编译过程”,接下来才是重点 —— 如何影响编译状态?强调一下,webpack 的插件体系与平常所见的 订阅/发布 模式差别很大,是一种非常强耦合的设计,hooks 回调由 webpack 决定何时,以何种方式执行;而在 hooks 回调内部可以通过修改状态、调用上下文 api 等方式对 webpack 产生 side effect。
比如,EntryPlugin
插件:
class EntryPlugin {
apply(compiler) {
compiler.hooks.compilation.tap(
“EntryPlugin”,
(compilation, { normalModuleFactory }) => {
compilation.dependencyFactories.set(
EntryDependency,
normalModuleFactory
);
}
);
compiler.hooks.make.tapAsync(“EntryPlugin”, (compilation, callback) => {
const { entry, options, context } = this;
const dep = EntryPlugin.createDependency(entry, options);
compilation.addEntry(context, dep, options, (err) => {
callback(err);
});
});
}
}
上述代码片段调用了两个影响 compilation
对象状态的接口:
-
compilation.dependencyFactories.set
-
compilation.addEntry
操作的具体含义可以先忽略,这里要理解的重点是,webpack 会将上下文信息以参数或 this
(compiler 对象) 形式传递给钩子回调,在回调中可以调用上下文对象的方法或者直接修改上下文对象属性的方式,对原定的流程产生 side effect。所以想纯熟地编写插件,除了要理解调用时机,还需要了解我们可以用哪一些api,例如:
-
compilation.addModule
:添加模块,可以在原有的module
构建规则之外,添加自定义模块 -
compilation.emitAsset
:直译是“提交资产”,功能可以理解将内容写入到特定路径 -
compilation.addEntry
:添加入口,功能上与直接定义entry
配置相同 -
module.addError
:添加编译错误信息 -
…
Loader 介绍
=========
Loader 的作用和实现比较简单,容易理解,所以简单介绍一下就行了。回顾 loader 在编译流程中的生效的位置:
流程图中, runLoaders
会调用用户所配置的 loader 集合读取、转译资源,此前的内容可以千奇百怪,但转译之后理论上应该输出标准 JavaScript 文本或者 AST 对象,webpack 才能继续处理模块依赖。
理解了这个基本逻辑之后,loader 的职责就比较清晰了,不外乎是将内容 A 转化为内容 B,但是在具体用法层面还挺多讲究的,有 pitch、pre、post、inline 等概念用于应对各种场景。
为了帮助理解,这里补充一个示例:Webpack 案例 – vue-loader 原理分析。
附录
==
源码阅读技巧
- **避重就轻:**挑软柿子捏,比如初始化过程虽然绕,但是相对来说是概念最少、逻辑最清晰的,那从这里入手摸清整个工作过程,可以习得 webpack 的一些通用套路,例如钩子的设计与作用、编码规则、命名习惯、内置插件的加载逻辑等,相当于先入了个门
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V获取:vip1024c (备注前端)
总结:
-
函数式编程其实是一种编程思想,它追求更细的粒度,将应用拆分成一组组极小的单元函数,组合调用操作数据流;
-
它提倡着 纯函数 / 函数复合 / 数据不可变, 谨慎对待函数内的 状态共享 / 依赖外部 / 副作用;
Tips:
其实我们很难也不需要在面试过程中去完美地阐述出整套思想,这里也只是浅尝辄止,一些个人理解而已。博主也是初级小菜鸟,停留在表面而已,只求对大家能有所帮助,轻喷🤣;
我个人觉得: 这些编程范式之间,其实并不矛盾,各有各的 优劣势。
理解和学习它们的理念与优势,合理地 设计融合,将优秀的软件编程思想用于提升我们应用;
所有设计思想,最终的目标一定是使我们的应用更加 解耦颗粒化、易拓展、易测试、高复用,开发更为高效和安全;
一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V获取:vip1024c (备注前端)
[外链图片转存中…(img-cNT5KBoP-1712784053383)]
总结:
-
函数式编程其实是一种编程思想,它追求更细的粒度,将应用拆分成一组组极小的单元函数,组合调用操作数据流;
-
它提倡着 纯函数 / 函数复合 / 数据不可变, 谨慎对待函数内的 状态共享 / 依赖外部 / 副作用;
Tips:
其实我们很难也不需要在面试过程中去完美地阐述出整套思想,这里也只是浅尝辄止,一些个人理解而已。博主也是初级小菜鸟,停留在表面而已,只求对大家能有所帮助,轻喷🤣;
我个人觉得: 这些编程范式之间,其实并不矛盾,各有各的 优劣势。
理解和学习它们的理念与优势,合理地 设计融合,将优秀的软件编程思想用于提升我们应用;
所有设计思想,最终的目标一定是使我们的应用更加 解耦颗粒化、易拓展、易测试、高复用,开发更为高效和安全;
一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
[外链图片转存中…(img-QbYFCeG7-1712784053384)]