微前端场景聊聊资源缓存问题
跟普通用户聊起缓存,一般首先会联想到手机和电脑的缓存,而且是个相对不好的东西,经常弄个某某安全管家,阶段性清理下,才会心安。尤其让我印象深刻的,是早期 Windows95 98 的年代,一进桌面,疯狂右键点刷新,等到刷新到界面很流畅的时候才会停下来去干别的事。是不是说的就是你?
在开发者眼里,缓存的场景就会有很多。我们先来看下,维基百科对其的定义
原始意义是指访问速度比一般随机存取存储器(RAM)快的一种 RAM,通常它不像系统主存那样使用DRAM技术,而使用昂贵但较快速的SRAM技术。
相信学过计算机组成原理的同学再去翻翻自己的课本,也会见到相类似的定义。当然随着时间的迁移,缓存的概念不仅仅指的是 SRAM 或者 CPU 内部的一级、二级乃至三级缓存,有个更加广泛的使用
如今缓存的概念已被扩充,不仅在 CPU 和主内存之间有 Cache,而且在内存和硬盘之间也有 Cache(磁盘缓存),乃至在硬盘与网络之间也有某种意义上的 Cache── 称为Internet临时文件夹或网络内容缓存等。凡是位于速度相差较大的两种硬件之间,用于协调两者数据传输速度差异的结构,均可称之为 Cache。
我们今天要聊的缓存,就指的是浏览器的缓存,主要与服务器资源文件之间的问题,以及如何运用。
笔者所在的团队所使用的微前端,从概念上来说的确和完整的微前端之间有些差距,并没有像 qiankun.js 或者其他方案一样,使用 spa 或者 WebCompent 等实现 css 隔离,js 隔离等。主要原因并不是对那些技术实现有迟疑或者技术水平问题,更多的是在考虑合理和可靠的迁移路径。因为在任何一个公司的技术团队,都要考虑投入产出比,还有风险问题,这是相纯技术的升级和迁移,并不实际给用户带来业务功能和实际体验上的提升,因此我们还是用着最原始的微前端方案,什么叫原始,原始到什么地步,有兴趣可以加我微信聊聊,哈哈。
当前笔者团队的问题主要来自于,每当用户切换前端应用,就会完整地从服务器拉取一份资源文件,怎么保证一定是最新的呢?有个大聪明在一早就这么实现的,资源请求 url 后面加?{Date.now()}
,是不是也有同学是这么干的。这当然也是个解法,而且我们团队一直用了很久。
虽然保证了资源一定是最新的但是实际给用户带来的体验就是在切换应用的时候,资源加载时间过长,实际感官就是白屏时间有点长,虽然 To B 应用只是内部用户,但是还是值得投入思考和分析的。
于是乎我们看看,能否有一个简单有效的方案,在保证应用升级后一定能够加载到最新的资源,在应用未升级的时候还是能够利用浏览器的缓存去快速加载的问题。
理论基础
HTTP 请求缓存存在于相同 URL 的 get 请求,非 get 请求(idempotent request)在默认规范不存在缓存,当然也可以特殊处理
在 HTTP get 请求 url 后面添加随机字符串或者动态时间可以避免资源被浏览器缓存
http 缓存根据是否需要重新向服务器发起请求来分类,如图所示
浏览器缓存根据存放位置可以分为内存缓存和磁盘缓存,这个不展开论述
其他更多细节的 http 缓存及 http 协议本身的知识可以参考 w3c 相关规范或者《图解 HTTP》
前端编译的目标
1.资源加载和转化,webpack 的 loader+plugin,loader 用来支持更多的文件类型,比如 jsx,css,ts,json,jpg,plugin 用来做更多的转换和预处理,包括 less 转成 css,根据目标浏览器配置做兼容性处理,js 语言版本的问题
2.还有 polyfill(垫片)的问题,比如 es6 es7 编译到 es5 ,新语法问题可以通过 ast 编译转化后降级,但是新增的 api 是没有办法
解决的,比如 Array.filter,Promise ,这种就通过 polyfill 填充不足的 api,但是也需要考虑目标浏览器版本,否则 polyfill 代码部分就会非常大反而影响加载速度
3.按需编译,拆分,压缩混淆等等。其他的不过多赘述参考 webpack5 文档
4.这里有个非常重要的点,webpack 在打包过程中会生成 manifest.json,此文件详细记录了入口文件地址,因此可以依赖这份信息,进行入口动态文件名的获取,为方案优化提供了可能(其他打包工具也一样会生成,这是个行业共识)
这里有几个注意点
- 通常情况下 GET 请求会产生缓存,那么我们请求 manifest.json 的时候可以通过添加
?xxxxx
动态后缀的方式,也可以使用 POST 去请求来解决 - 建议给所有的资源文件在编译的时候都加上 hash 后缀
- 因为当前团队的微前端应用超过了 30 个,有部分老应用常年使用但是没有更新,迭代过程可能会有遗漏,笔者团队实现了从打包工具到技术组件等全部自研接管的目标,因此老应用在打包工具是否升级的区别就在于是否开启了 manifest.json,这里大家不要误解,因此笔者为了防止遗漏,在门户的代码侧依然兼容了老应用的加载方式,也就是强制拿最新资源文件的方式
版本号管理方案
笔者的团队对前端资源文件实现了 Tag 号的版本管理,但是在部署形态上并没有区分版本,也就是在 Nginx 资源文件代理上,并没有实现类似/view/web/${app.name}/${app.version}/xxx 的概念
参考在老东家的时候实现的方案,如果在部署形态上有前端资源版本的概念,那么这个缓存的问题的解决方案就是另外一个形态。简而言之,就是在门户侧实现有前端应用管理模块,实现应用注册,资源地址配置和版本号管理等,在门户加载微前端应用的时候再动态获取当前微前端资源和版本号信息进行动态加载,那么就可以不去采用这么一种实现方案,因为版本号就能区分。随之而来的一个问题就是 CI/CD,已经版本发布上的一个成本就是要在版本发布后更新这份应用注册表。这一点仁者见仁智者见智,还是一句话看团队的实际场景和诉求,投入产出比和风险一定是个绕不开的话题。
写在最后
2022 年是个不平凡的一年,是不是大家都已经是“杨过”,或者“小阳人”的状态,还希望大家注意身体。22 年的软件圈,互联网圈都在经历大裁员,行业寒冬,经常逛掘金和朋友圈,发现不少同学也在经历,比起身体上被病毒的折磨,相信工作和事业上的打击才是最沉痛的,祝愿大家能够度过难关。
笔者中间也断更了很久,有忙的原因,也有迷惘的因素。年底了,述职,复盘,回溯这一年,展望下一年的时候,感受着天气和行业的寒气,的确让人不寒而栗,肩上的担子,心中的抱负,思想的枷锁,都得一一闯关。