转载

前端工程化 - 聊聊 Webpack v3 到 Webpack v5 的核心架构变迁

随着前端工程化的持续发展, Webpack 作为一个核心框架, 在整个打包构建中占据了主导地位, 但同时 Webpack 从最初的相对简单的配置也变得日益复杂和庞大, 从我的经历来讲, 到目前的 v5 版本, 我所理解的 Webpack 架构经历了 3 次比较大的变迁, 由于职业病的关系, 本着对于架构的关注和热爱, 我将这些架构变迁整理成文, 希望和各位一起聊聊其中一些有趣的设计, 和变迁的细节, 遂有此文

正文

v1 - v3 单图架构

Webpack 诞生之初的口号是一切皆可打包, 正如官网首页上的那张图所显示的那样, 可以说从 Webpack 开始, 前端工程才有真正意义上的 bundle, 过去的 grant gulp 解决了流程问题, 但始终没有解决如何将资源打成一个包, 这个最朴素的问题, Webpack 补齐了前端工程化的这块短板, 也同时将前端工程化推向了一个崭新的领域.

这个时期差不多是 v1 - v3 这三个版本, 我记忆中, 从 v1 - v3 Webpack 的升级没有太过重大的变更, 都还是比较平滑, 这种平滑的背后是核心架构在设计上的稳定, 先让我们来看看, 这一时期 Webpack 带来的几个主要概念

  • entry
  • output
  • Module
  • chunk

在早期的核心架构中, 将这 4个概念融合成一张图差不多是这样

前端工程化 - 聊聊 Webpack v3 到 Webpack v5 的核心架构变迁

解释下这张图的意思, Webpack 通过 entry 指定的入口文件进行分析, 这个分析过程么, 一言难尽, 反正你只要知道, 对于 Webpack 来讲 require import 等等各种模块导入语法都要支持, 一顿分析猛如虎, 这里 Webpack 主要用的是

acorn.js 这是个用 JavaScript 写的非常快的高性能解释器, 用于将 JavaScript 代码转成抽象语法树, 你说为啥不用 babel ? 首先 babel 主要针对的还是各种版本的 JavaScript 的语法转换, 但是 babel 其实不解决模块解析这个问题, 所以对于 Webpack 来说用 babel 太重了. 而且也不能彻底解决问题, 何况 v1 的时候不也没 babel 啥事么.

让我们回到上面的图, 这里重点要说下 chunk module 以及连起来的那几条线, 在 Webpack 的核心架构里, 最核心的内容就是 chunk, 可以说 chunk 这个概念, 以及围绕 chunk 的一系列设计是整个 Webpack 的核心的核心, 基础的基础, 没有 chunk Webpack 可以说就等于没有了灵魂. 然后是 module, 在 Webpack 的设计里 module 是文件的抽象映射, 配合 loader 组成了 bundle 的实体内容, 不像 chunk 这么抽象, module 非常具体, 一个文件 → 一个module.

上面讲了这么多, 其实还没点到题, 标题里我写了一个 单图架构 这个单图指的就是 ChunkGraph, 一个有 chunk 构建的图结构, chunk 内包裹了 module, 这个基于 ChunkGraph 的单图架构就是当时 Webpack 的核心架构, Webpack 的依赖分析, 包括 CommonsChunkPlugin 的公共代码优化, 打包等等都是基于这个核心架构来完成的.

那为什么后来到 v4 就变了呢, 经历过 v3 到 v4的同学应该有感触, Webpack v4 升级在当时是非常不平滑的, 好多插件不兼容, 迁移成本很大, 这里一个核心原因其实在于这个 CommonsChunkPlugin 的优化已经不能适应当时前端工程化对于拆包的诉求, 于是就有了 v4 的重大变更以及后来引入的 SplitChunksPlugin

v3 - v4 从单图架构到双图架构

当时我记得是 18年吧好像, 反正那个时候 Webpack v3 有个问题一直优化不好, 就是拆包, 怎么拆怎么不舒服, 就总是拆不干净, 总有重复代码, 后来我研究了下, 问题就出在 CommonsChunkPlugin 的优化策略上, 不过这也不能怪 Webpack, 在 v3 这种单图架构下, 这么做无可厚非, 那这个优化策略是什么呢?

其实很简单, 在 ChunkGraph 中, 所有的 Chunk 都有一个 parent info, 标注了自己的父级 Chunk 是哪一个, 然后假设有一个 module 它所在的 Chunk 的 parent 是全的, 那说明这个 module 是一个可以被 common 化的 module, 所以你看这个 CommonsChunk 就知道这插件干吗的, 就是搞一个 Chunk 把这些公共的 module 都存起来, 然后呢? 再添加回 ChunkGraph, 然后因为每个 Chunk 都得有 parent 那个 info嘛, 所以 Webpack 默认 CommonsChunk 是 parent chunk, 这么做可能早期问题不大, 不过后来就有问题了, 主要是 2 点.

  • 因为 CommonsChunk 里面是一堆公共依赖, 但是没啥先后顺序, 所以很难异步加载, 你必须尽可能同步
  • 另外么就是反正你要不要不重要哈, 不管里面的 module 你要不要, 反正这个 chunk 一加载就全来了, 拖家带口的非常不友好

还有一些问题, 比如难以理解, 确实难以理解, module 之间有关系, 但是必须通过 chunk 来建立, 这其实会有很多逻辑重叠在一起, 可能不搞这个 CommonsChunk 还行, 搞了之后分析起来巨复杂. 反正我估计 Webpack 的人也是要疯了. 不想在搞了, 觉得在这个基础上除了把自己搞神经病应该是不会有啥好结果了, 所以活人能让尿憋死? 搞, 重新设计, 重新架构, 重构

于是就有了 v4 里的双图架构, 这里的双图是原有的 ChunkGraph, 另一个是 ModuleGraph. 然后 CommonsChunk 变成了 SplitChunk, 并且把原来的 parent chunk 和 child chunk 的关系变成了 parent chunk group 和 child chunk group 的关系, 这里 chunk group 也是个新增的概念, 其实就是为了把 chunk 变纯粹, 父子关系抽象到 chunk group 上去了, 从诶反正就这样变成了, 你说咋搞就咋搞, Webpack 管这个叫启发式, React 也有个启发式, 感觉比较相似, 双图架构的好处显而易见, 相当于把搅和在一起的两张网给拆开了, Chunk 依然包含 Module 但是在依赖分析上, ModuleGraph 自己玩自己的, Chunk 成了一个纯粹的抽象概念, 这里我就要提之前老提的那个架构思想, 设计和实现分离, Chunk 就是一种设计, Module 却是一种实现, 单图架构中把设计和实现粘在一起, 问题显而易见嘛, 不然也不会有后来的双图架构了.

至于为啥拆了就可以好好的拆包了? 因为 Chunk 的分析边独立了, 也没有父子结构了, 扁平了, 这些就好搞了, 想怎么组合怎么组合, 想异步就异步, 只要把 Chunk 本身做个分类, 比如有 inital chunk 和 non-inital chunk, 后者就是用来搞异步的, 前者就是通过 entry 形成的那个 chunk

所以从 v3 到 v4 Webpack 的拆包功能一下牛逼了一大截, SplitChunk 非常灵活, 可控性也强了很多, 不过前端工程化发展的也非常快, 从 v4 到现在 2年多的时间, 微前端 被抛了出来, 这对前端工程化又提出了新的要求, 自然作为与时俱进的 Webpack 团队也不能落后不是, 于是就有 v5, 新鲜出炉的热乎特性 Module Federation , 同样的 v5 也带来了新的架构设计

v5 - 基于容器的架构

因为 v5 目前依然处于 beta 阶段, 所以这里写的一些概念可能随后都有可能发生变化, 最终以官网发布的版本为准, 我会尽可能更新文章以匹配官方的内容

其实之所以有 v5, 如果没有 v4 的 ModuleGraph 架构升级也是做不到的, 因为你看 Module Federation, 是基于 Module 的, 如果还是 v3 的 单图架构, 包都拆不出来不要说搞联盟了, 最多是个独裁, 所以架构是演进出来的, 不存在跳跃式的架构, 每一次演进都是有迹可循, 相互联系的 , 所以 Module Federation 要解决什么问题?

Module Federation 要解决的核心问题是, 如何在生产环境的 runtime 里支持构建, 并且还不影响原有的开发时的构建模式, 从而支持构建的水平拆解. 简单来讲呢就是你可以线上加载别的 bundle 里的模块而不用线下重新 build 了, 很神奇是不是, 如果你受够了 npm 引用, 对方一升级, 你要重新打包构建发布, 那确实很神奇, 解决了大问题.

所以要做到这一点, 原有的架构设计肯定不满足嘛, 于是就有了新概念 Container

Container 里有一个 fileName , 就是 output 的别名, 然后 Container 也有自己的 entry, 通过 name 来映射到你自己配置里的 entry, Container 还有一个重要的概念就是拿 module 当接口, 没错, 你没看错, 你就把 Container 想象成啥一个服务, 对 api 是啥就是里面的 module, 所以你要这个 module 就很简单, 去请求这个 Container 就好了嘛, 所以 Container 有一个 remotes 属性用来标识一个请求源.

所以在 v5 的核心架构里, 除了原有的双图架构, Webpack 革命性的提出了 Container 的概念, 但我觉得最革命的还是拿 module 做 api , 这个设计真的是…..脑洞巨大

因此 Container 是完全基于运行时的一套架构, 原来 Webpack 的运行时其实非常简单, 如果你看过他的 runtime 包裹代码的话, 那现在就不一样了, 变得异常复杂, 因为 v5 还未发布, 所以后续是否有变化不好说, 从目前看, 这套架构设计上还是有些比较难理解, 所以我觉得还有很大的优化空间, 如果你感兴趣, 可以自行查阅相关的文档, 包括示例, 官方给的还是挺全的. 当然我写的这些应该是没有的, 毕竟是我总结的 :grinning:.

后话

Webpack 的架构设计非常有趣, 通过学习和探索, 也让我获益良多, 也希望以此文唤起大家对架构设计上的探索和兴趣

原文  https://juejin.im/post/5f1ac4725188252e4839cfe6
正文到此结束
Loading...