前言
微前端
的实现好像也用不到什么突然出现的新技术,或者有什么高深莫测的概念,可近两年它就火了。早在 2016 年被 Thought Works 提出后,也好像没有掀起什么波澜,直到 19 年 20 年,qiankun
等一众微前端的框架出现后这个词才渐渐热起来。在这个概念提出前,也有 iframe
这样的解决方案达到了类似的效果,只是它不够标准化,也没有一个高大上的名字,我们只管叫它应用嵌套或者就叫嵌套一个页面(论起名字的重要性)。
背景
前东家的老板在“双减”出现前,开始做领域内(少儿编程)的转型,由卖课转向做“少儿编程社区”。我作为公司内的 scratch 砖家在第一个版本上线后自然而然地开始做 scratch 在各个页面和端上的性能优化,希望它在无缓存状态能够加载得更快。
行动
寻找一击即中的优化方向
已上线的版本早在课程系统重构的时候就已经做了一版 scratch 瘦身了,无非就是使用 webpack-bundle-analyzer
跑一圈看看哪里有大体积文件,有重复的 npm 包,然后对症下药解决问题。由于没有足够的时间,看到 scratch-vm/src/extensions 下的一堆扩展我是比较无奈的,尤其是 music 下面一堆 .mp3 文件,为了保证原汁原味的scratch,它自带的能力我们一般是不会去做移除的。scratch 的扩展打个比方就好像一个 npm 源,里面有无数的第三方包,但是你不可能一个项目需要把所有包全部装上把。scratch 的现状就是这样的——不管你用不用,我都给你怼上了,随着我们加更多扩展,js 就会越来越大。
尝试过 dynamic import 方案,作为个包来说,不能做到开箱即用。如果转译后发布,则需要拷贝 chunks。如果跳过 dynamic import 语法发布,需要在主应用里添加转译路径
和产品聊了后续的规划后,得知“丰富的扩展”也是我们后续要主打的一个特色,所以这个优化点能给当下和未来的带来非常直接的收益。
实施
创建新的项目
不管是 monorepo 还是独立的仓库,我们都需要单独创建一个项目,带来的最大好处就是独立部署
。
定义数据解构
1 | interface ExtensionMap { |
我们以最大的内置扩展music
为例进行迁移,得到这样的一个对象
1 | export default { |
编写 webpack 配置
首先你需要固定 webpack 产出的主文件的名字,如果不这么做的话,将导致入口变成动态的。
1 | const webpackConfig = { |
我们只需要固定入口文件的名字,产生的其他 chunks 依然让他带上 hash,这样可以利用缓存。
部署
将 webpack 的构建产物部署到服务器,让我们能够访问到主文件如 https://yousite.com/scratch-extension/main.js
如果不是协商缓存的策略那么路径应该是
1 | `https://yousite.com/scratch-extension/main.js?v=${Date.now()}` |
修改 scratch 的扩展加载逻辑
首先我们找到 scratch 的扩展加载逻辑
简单解释下这里的流程
- 尝试从内置扩展列表中查找扩展并加载
- 扩展在内置列表不存在则从网络直接加载
然后我们在扩展在内置列表不存在
这条逻辑后面插入一段从我们组件服务,下面是伪代码
1 | new Promise(resolve => { |
收益
- 扩展作为一个单独的服务,独立开发,独立部署,构建即生效,减少发布到私有源再更新其他项目的的成本
- 扩展的 js 代码真正做到按需加载,新的扩展几乎不影响 main.js 和 scratch 本身的大小。
- 减少主应用的构建工具在这里花费的时间
复盘
将扩展服务的入口文件 hard coding 到代码里始终不太优雅,如果有必要,我们可以将 scratch-extensions 作为 package 发布,然后在代码里 import,然后通过 webpack 的 external 能力,在主应用上配置scratch-extensions 的外部链接来将它提取出去。甚至我们可以配合 webpack5 的 Module Federation(模块联邦)达到一样的效果