文章 webpack 版本为 3.6.0
前言
随着掌握的前端基础知识越来越多, 对技术的要求逐渐不满足于实现即可, 技术到了瓶颈期, 自己也曾尝试写过一些开源库, 不过很少有满意的作品, 通常没迭代几个版本就没有耐心继续维护了. 通常是面临的情形是前期设计思路太过简单导致后期扩展的时候需要重构大量的代码 (GG 吧~), 就好比一坨屎, 再怎么装点, 都很难把它当成蛋糕吃下去.
我认为, 突破这个瓶颈的关键就是学会深入理解优秀开源库背后的思路. 有人可能会说, 我用 xxx 已经很久了, 能够熟练使用它解决各种棘手问题, 对于它, 我已经充分理解了. 我想说的是, 即便你对于它的使用已经达到了炉火纯青的程度, 但是站在使用者角度理解再 "深" 能有多深呢, 不过是坐井观天罢了.
为什么 Webpack
目前为止, Webpack 已经拥有 39.9k 的 star, 在前端代码打包器领域内应该算是无敌的存在了吧. Webpack 强大的生态圈和丰富的解决方案使得我们在日常开发中很难逃脱它的魔爪. 如果能学习到它背后的思路, 对于技能树的完善和水平层次的提高应该是非常有好处的.
概要
如果要全面总结 webpack 的实现, 估计写 10 篇文章都不一定够. 为了更加清晰地 get 到 webpack 的设计思路, 会隐去 webpack 的大部分功能实现.
以实现简单的 js 模块打包功能为背景, 文章分为 3 部分:
BundleBuilder 基本架构
Webpack 基本架构
学到了些什么
相信你在阅读完本文后会对 Webpack 的架构有个大概的了解, 这应该会对你继续深入理解 webpack 其它功能的实现以及编写插件会有所帮助.
BundleBuilder 基本架构
简单到不能再简单的 js 模块打包器
示意图
BundleBuilder 对象
BundleBuilder 对象接收并处理外部配置
根据配置选择不同的 ModuleResolver
使用 ModuleResolver 接收配置得到最终文件内容
生成打包后的文件
ModuleResolver 对象
接收从 BundleBuilder 传进的配置
解析入口文件内容
提取子模块路径, 并递归地解析子模块
将引用的模块路径替换为模块 id 最终生成模块文件
webpack 基本架构
这个接下来依次讲解 webpack 中几个重要对象之间的关系, 会以各自的视角描述几个重要的过程. 当然, 就单单这几个对象还不能完全地描述流程上的所有内容.
Tapable 插件功能
webpack 4.0 的插件系统已经完全重做并将 Tapable 更新到了 1.0.0
在正式介绍几个核心对象之前, 你需要了解一下 Tapable 类.
简单来说, Tapable 为一个对象提供了插件功能. 如果你用过 vue.js 或者 React.js 之类的框架, Tapable 就是为某个对象提供了相当于组件的生命周期功能, 在外部你可以通过调用这些生命周期钩子监听该对象.
当然, 你还可以在外部手动触发对象的某个生命周期.
如果你想详细了解 Tapable 的 API 可以参考这里 http://blog.zxrcool.com/2017/10/22/Webpack原理分享(一)/ (文中版本为 0.2.8)
Webpack 主函数视角
最宏观的视角
1. 合并外部与默认配置
2. 配置并创建 compiler
3. 在 compiler 启动前触发 compiler 上的若干生命周期
其中生命周期包括: environment,after-environment,entry-option,after-plugins,after-resolvers
4. 启动 compiler
5. 将 compiler 运行后得到的状态信息打印出来
Compiler 视角
1. 正式运行前依次触发 before-run 和 run 生命周期
2. 创建 params 对象并触发 before-compile 生命周期
3. 触发 compile 生命周期并创建 compilation 对象
4. 触发 this-compilation 和 compilation 生命周期
5. 触发 make 生命周期并调用 compilation.finish()
在 make 阶段调用了 compilation.addEntry(), 开始构建模块树, 构建完毕后调用 compilation.finish(), 记录报错信息
6. 调用 compilation.seal() 并触发 after-compile 生命周期
compilation 在 seal 过程中做了很多工作, 在 compilation 视角部分会讲到, 现在只需知道 seal 过后 compilation 生成了 assets 对象以供 compiler 生成文件
7. 拿到 assets 并在生成每个 assets 对应的文件
8. 将警告信息和文件大小信息合成为 stats 状态信息
9. 触发 done 生命周期并将 stats 状态信息交给 webpack 主函数
Compilation 构建模块树视角
当 compiler 命令 compilation 构建模块树之后 compilation 都做了些什么
1. 使用 moduleFactory 创建空 module
2. 命令 module 自行构建自身属性, 比如依赖的子模块信息 (dependency)
调用 module.build() 进行构建模块自身属性
3. 递归地重复 1 和 2 的操作, 生成模块树
4. 将模块树记录到 chunk 中
Compilation 的 seal 视角
1. 配置 chunk
2. 将所处模块树深度和引用顺序等信息记录在每个模块上
3. 将所有模块按照引用顺序排序
4. 触发 optimize-module-order 生命周期并按照排序后的模块顺序为每个模块编号
5. 使用 template 对象渲染出 chunk 的内容 source
6. 拿到 source 后生成 asset, 添加到 assets 中
学到了什么
引入插件系统
1. 存在的问题
可以看到, BundleBuilder 的架构中完全没有为第三方提供接口, 后期当然也可以做成根据不同的外部配置项来实现一些有限的定制化需求.
但是, 这样为了保证功能的多样性, 会频繁修改打包器的内部实现. 这种做法会使得整个打包器的稳定性不足, 最终非常臃肿, 维护困难.
2. webpack 的做法
反观 webpack, 它使用了一种非常聪明的方式. 在保证基本架构的前提下, 为主流程上的大部分对象都引入插件系统, 使用者可以获取到这些对象, 并且在一些特定的时候运行使用者提供的代码. 这样一来, 社区的逐渐壮大保证了功能的多样性, 还把稳定性不足的风险留给用户去处理, 提高了整个打包器的可维护性.
过程粒度细化
1. 存在的问题
可以看到, BundleBuilder 最终生成文件内容只有一个过程, 就是调用 ModuleResolver 获取字符串. 当这个过程中的某一阶段需要独立进行的时候, 难免会要重构代码. 如果内部实现是比较松耦合的, 那么重构的工作会比较轻松, 但是像现在 BundleBuilder 这种实现, 显然要做的工作并不少.
2. webpack 的做法
从接收配置到生成文件内容, 从比较宏观的角度, 分为构建, 封装, 生成文件内容, 三部分.
保证了内部修改的灵活性. 如果要对过程再细分或者添加过程, 实现起来会比较方便.
丰富了对外扩展的接口. 很显然, 由于 webpack 引入了插件系统, 细化过程粒度应该是必然选择, 这样会有效地增加用户对整个打包过程的自定义能力.
提升了代码的可维护性. 当打包器在运行时出现了 bug, 粒度越小, 越加方便定位问题.
更多类的抽象
1. 存在的问题
在 BundleBuilder 中, 对于每个模块仅仅是通过路径读取它的文件内容, 然后分析其子模块的信息, 最后生成处理后的模块内容. 这些都是过程. 如果后面迭代时需要在打包后输出一些 log, 如模块警告, 模块路径等与模块相关的信息. 以面向过程的编程方式当然也可以实现, 但这样难免会增加实现难度, 降低代码可读性.
2. webpack 的做法
稍微搜索一下, 不包括自带插件, webpack 总共有 200 多个用 Class 声明的类.
结构化的数据. 创建一个类就意味着我们能统一很多有相同抽象含义的对象创建同样的属性, 比如 Module 类, 它可以记录很多与模块相关的信息.
方便扩展不同种类的对象. 比如模块类, 可以通过继承的方式衍生出, 普通 js 文件模块, CSS 文件模块等等.
多类意味着有承担不同职责的对象. 明确的职责分工, 比如 compiler 仅仅负责 compilation 的创建, 文件的生成和信息状态的合成. ModuleFactory 负责创建 Module. 一旦出了问题方便定位到责任人, 降低了各个工作的耦合度.
对象间的解耦. 比如 compilation 和 Module 两个类, webpack 其实也可以直接使用 compilation 来直接创建 Module, 但是一旦 Module 的种类增加, 不可避免地需要在 compilation 中写一些条件语句, 这样, 创建 Module 这部分的代码会让本来就有很多事情要做的 compilation 变得更加庞大. 所以 webpack 引入了 ModuleFactory,compilation 只需调用 ModuleFactory 来创建 Module 就好, 创建部分的逻辑则被分布在了 ModuleFactory 中, 将 compilation 与 Module 解耦, 两者中一方发生变化, 只需在 ModuleFactory 中增加逻辑即可.
感受
由于 webpack 过于庞大, 看源码的过程感觉是在修行. 写这篇文章之初准备深入到一些技术细节, 后来感觉意义不大. 也尝试过列举在简单 js 模块打包流程上涉及到的默认插件, 写出来像 API 手册, 如果完全写完, 体量可能都接近半本书了. 最后, 决定拿小学 3 年级画画水平, 将最基本的架构关系画出来.
最大的感受就是: 当你真的准备设计一个库的时候, 应该在实现之前充分列举可能的应用场景, 将充分抽象出稳定的基本架构, 然后将难办的部分, 复杂度很高的部分, 或者说定制化需求比较多的部分, 采用开放插件的方式扔给使用者去解决.
来源: https://juejin.im/post/5ad89d6ff265da0ba76f44fe