背景
静态资源的快速加载和高可用性对用户体验至关重要。随着 CDN 的推行,单一云服务商在遇到特殊情况时(如网络中断、服务故障等),会导致静态资源加载失败,从而影响用户体验和业务连续性。
所以,单靠一个云服务商已经不能满足我们的需求了,我们需要更强的容灾能力。需要对资源进行多云自动切换。
我们的最终目标是“多CDN架构”,具体实现方式如下
在源站上配置多个CDN域名,每个CDN域名对应一个CDN服务提供商的节点。这样,不同的CDN节点可以通过不同的域名访问同一个源站。
基于这种 “多 CDN 架构 ” 来应对单个云厂商在遇到特殊情况时 可以对资源进行自动切换,以保证网站的正常运行。
如何实现
静态资源的分类
静态资源可以大致分为两种类型:
同步静态资源
跟随页面组合好之后一起返回 | 接口返回的cdn地址
特点:能够灵活控制资源cdn域名
异步静态资源
由前端打包工具,建立关联关系后,构建工具异步拉取
特点:由前端在构建时,根据不同的环境写死的cdn域名,在构建时已经确定,无法在运行时动态替换
原理
所以我们的实现主要分为三部分:
- 如何自动获取加载失败的静态资源(同步加载的
<script>
,<link>
,<img>
)并重试 - 如何自动获取加载失败的异步脚本并重试
- 如何自动获取加载失败的背景图片并重试
最先,我想到的是动态去修改```__webpack_public_path__ ``(构建时修改,运行时生效)
伪代码如下:
1 | // 代码中写死的publicCDN地址: |
优点:在运行时就会直接替换,不会触发error后再加载,过程比较平滑
缺点:需要修改构建配置,现在主流是webpack,后续构建工具多了之后,还要对不同的构建做不同的改动
显然,这不是最优选,不仅需要每个业务中都去修改配置,后续如果升级了其他构建工具,可能还会出现其他预想不到的问题。
方案二:
通过挂载全局error事件,捕获资源失败场景
1 | window.document.addEventListener('error', errorHandler, true); |
然后通过errorHandler来处理对应异步资源的cdn域名替换
伪代码如下:
1 | function errorHandler(e) { |
优点:无需业务侧的配合,比较通用,既能兼顾老的页面也能支持新的页面
缺点:会触发资源的error事件之后,再次挂载对应的修改后的cdn
但这种方式无法对webpack构建的异步产物生效,因为在一个页面有多个异步chunk请求时,webpack是通过promise.all来处理异步chunk的,当一个失效后,Promise会进入rejected状态,导致再无法转为其他状态,会阻塞其他异步文件加载。
如何打破这种局面?摆在我们面前的只有两条路:
- 使用 webpack 插件,在编译期改写该段代码。
- 使用 monkey patch 对浏览器的原生方法进行改写。
为了降低集成成本,我们选择了第二种方案,即在运行时动态改写 document.createElement
, Node#appendChild
等方法。
大体思路:
1 | document.createElement |
为了阻止webpack在onerror回掉直接进入reject流程,可以直接对浏览器原生方法的document.createElement以及Node#appendChild等方法进行重写,使其流程变为:
1 | document.createElement |
代码流程图:
优点:只针对script/link等标签的异步资源做拦截,无需担心掉入构建工具的rejected状态,从而影响后续异步资源的加载,也能配合最大重试次数的限制,能够及时终止循环重试。业务侧无需改动,公共模板挂载这段逻辑,并初始化即可。
看到这也许你会问了,
为什么要创建 虚拟 element 对象?
其实直接创建真实element,并且对真实element的onerror和onload做重写也ok,但是会被用户自定义的onerror和onload覆盖掉。
但是虚拟element就可以解决这个问题,可以将用户设置的onerror和onload挂载到虚拟对象上,通过ignore标识来判断,走用户设置的onerror还是默认重写的onerror。
那么 对页面上其他的document.createElement方法有没有影响?
没有影响,因为仅仅是在创建的时候多了一层包裹对象,本质上还是在调用原生createElement,并且在对创建的元素进行属性设置或者是插入节点动作时,都是取的真实的元素,而非虚拟对象。
1 | // hookElement |
页面中可能使用到document.createElement的场景
- Vue中的render函数
- Vue中的自定义指令
- webpack中的动态css/js
- vite中的动态css/js
- 用户(开发)操作动态添加DOM元素
最终效果
a. 前置条件申请 3 个 c d n 域名
b. 插入重试逻辑代码,体积2.2kb,可以直接放到页面上,无需使用cdn加载
c. 初始化重试方法
d. 屏蔽原异步js地址
e. 进入异步页面,验证重试
如何使用
1 | 使用起来非常简单,只需要初始化并传入域名列表即可: |
当使用以上代码初始化完毕后,以下内容便获得了加载失败重试的能力:
- 所有在
html
中使用<script>
标签引用的脚本 - 所有在
html
中使用<link>
标签引用的样式 (跨域 CSS 需要正确配置) - 所有在
html
中使用<img>
标签引用的图片 - 所有使用
document.createElement('script')
加载的脚本(如 webpack 的动态导入) - 所有
css
中(包含同步与异步)使用的background-image
图片
配置
assetsRetry
接受一个配置对象 AssetsRetryOptions
,其类型签名为:
1 | interface AssetsRetryOptions { |