# 静态资源重试

Web 项目上线后，在千差万别的网络状态中，访问页面难免会遇到前端资源加载失败的情况，占比或许不高，但一遇到，轻则页面样式错乱，重则白屏打不开，影响用户体验感受，紧急情况下甚至影响了用户的工作，属于非常严重的问题。

## 如何监控资源加载失败

### script onerror

我们可以给 script 标签添加上 onerror 属性，这样在加载失败时触发事件回调，从而捕捉到异常。

```markup
<script onerror="onError(this)"></script>
```

并且，借助构建工具 ( 如 webpack 的 script-ext-html-webpack-plugin 插件) ，我们可以轻易地完成对所有 script 标签自动化注入 onerror 标签属性，不费吹灰之力。

```javascript
new ScriptExtHtmlWebpackPlugin({
  custom: {
    test: /\.js$/,
    attribute: "onerror",
    value: "onError(this)",
  },
});
```

### window.addEventListener

上述方案已然不错，但我们也试想是否可以减少 onerrror 标签大量注入呢？类比脚本错误 onerror 的全局监控方式（详见：[脚本错误量极致优化-监控上报与 Script error](https://github.com/joeyguo/blog)），是否也可以通过 window.onerror 去全局监听加载失败呢？

答案否定的，因为 onerror 的事件并不会向上冒泡，window.onerror 接收不到加载失败的错误。冒泡虽不行，但捕获可以！我们可以通过捕获的方式全局监控加载失败的错误，虽然这也监控到了脚本错误，但通过 !(event instanceof ErrorEvent) 判断便可以筛选出加载失败的错误。

```javascript
window.addEventListener(
  "error",
  (event) => {
    if (!(event instanceof ErrorEvent)) {
      // todo
    }
  },
  true
);
```

通过监控数据分析，我们发现现实情况不容乐观。访问页面时存在资源加载失败的情况超过了 10000 例/天，且随着页面访问量的上升而增加。

另外，监控资源加载失败的方式不止这些，上述两种方式都属于较好的方案，其他的方式就不再展开。

## 优化资源加载失败

### 加载失败时，刷新页面 (reload）

有了监控数据后，便可着手优化。当资源加载失败时，刷新页面可能是最简单直接的尝试恢复方式。于是当监控到资源加载失败时，我们通过 location.reload(true) 强制浏览器刷新重新加载资源，并且为了防止出现一直刷新的情况，结合了 SessionStorage 限制自动刷新次数。

通过监控数据发现，通过自动刷新页面，最终能恢复正常加载占异常总量 30%，优化比例不高，且刷新页面导致了出现多次的页面全白，用户体验不好。

### 针对加载失败的文件进行重加载

#### 替换域名动态重加载

只对加载失败的文件进行重加载。并且，为了防止域名劫持等导致加载失败的原因，对加载失败文件采用替换域名的方式进行重加载。替换域名的方式可以采用重试多个 cdn 域名，并最终重试到页面主域名的静态服务器上（主域名被劫持的可能性小）

然而，失败资源重加载成功后，页面原有的加载顺序可能发生变化，最终执行顺序发现变化也将导致执行异常。

#### 保证 JS 按顺序执行

在不需要考虑兼容性的情况下，资源加载失败时通过 document.write 写入新的 script 标签，可以阻塞后续 script 脚本的执行，直到新标签加载并执行完毕，从而保证原来的顺序。但它在 IE、Edge 却无法正常工作，满足不了我们项目的兼容性。

于是我们需要增加 “管理 JS 执行顺序” 的逻辑。使 JS 文件加载完成后，先检查所依赖的文件是否都加载完成，再执行业务逻辑。当存在加载失败时，则会等待文件加载完成后再执行，从而保证正常执行。

手动管理模块文件之间的依赖和执行时机存在着较大的维护成本。而实际上现代的模块打包工具，如 webpack ，已经天然的处理好这个问题。通过分析构建后的代码可以发现，构建生成的代码不仅支持模块间的依赖管理，也支持了上述的等待加载完成后再统一执行的逻辑。

```javascript
// 检查是否都加载完成，如是，则开始执行业务逻辑
function checkDeferredModules() {
  // ...
  if(fulfilled) {
    // 所有都加载，开始执行
    result = **webpack_require**(**webpack_require**.s = deferredModule[0]);
  }
}
```

然而，在默认情况下，业务代码的执行不会判断配置的 external 模块是否存在。所以当 external 文件未加载完成或加载失败时，使用对应模块将会导致报错。

```javascript
"react": (function(module, exports) {
  eval("(function() { module.exports = window[\"React\"]; }());");
})
```

所以我们需要在业务逻辑执行前，保证所依赖的 external 都加载完成。最终通过开发 wait-external-webpack-plugin webpack 插件，在构建时分析所依赖的 external，并注入监控代码，等待所有依赖的文件都加载完成后再统一顺序执行。

至此，针对加载失败资源重试的逻辑最终都通过构建工具自动完成，对开发者透明。重试后存在加载失败的情况优化了 99%。减少了大部分原先加载失败导致异常的情况。

## 始终加载失败该怎么办

用户网络千变万化，或临时断网、或浏览器突然异常，那些始终加载失败的情况，我们又该如何应对呢？ 一个友好的提醒弹框或是最后的稻草，避免用户的无效等待，缓解用户感受。

## 总结

以上，便是对资源加载失败优化的整体方案，从如何监控加载失败、加载失败时重试、重试失败后的提醒等方面。大幅优化修正了加载失败的问题，也缓解着实遇到异常的用户使用体验。
