Skip to content

Latest commit

 

History

History
212 lines (164 loc) · 12 KB

README-cn.md

File metadata and controls

212 lines (164 loc) · 12 KB

English | 简体中文

静态资源自动重试

当页面中的脚本、样式、图片资源无法正常加载时,自动重试加载失败的资源。支持备用域名、动态导入(dynamic import),无需改动现有代码,仅需 3 KB (gzipped)。

Demo GIF

目录

安装

通过 npm 安装

$ npm install assets-retry --save

然后通过 webpack 配置 内联到页面的 head 标签中,并置于所有资源开始加载之前

直接通过 script 标签引用

如果你懒得折腾 webpack 配置,可以将 assets-retry.umd.js 直接内联到 <head> 标签中,并置于所有资源开始加载之前

快速上手

使用起来非常简单,只需要初始化并传入域名列表即可:

// assetsRetryStatistics 中包含所有资源重试的相关信息
var assetsRetryStatistics = window.assetsRetry({
    // 域名列表,只有在域名列表中的资源,才会被重试
    // 使用以下配置,当 https://your.first.domain/js/1.js 加载失败时
    // 会自动使用 https://your.second.domain/namespace/js/1.js 重试
    domain: ['your.first.domain', 'your.second.domain/namespace'],
    // 可选,最大重试次数,默认 3 次
    maxRetryCount: 3,
    // 可选,通过该参数可自定义 URL 的转换方式
    onRetry: function(currentUrl, originalUrl, statistics) {
        return currentUrl
    },
    // 对于给定资源,要么调用 onSuccess ,要么调用 onFail,标识其最终的加载状态
    // 加载详细信息(成功的 URL、失败的 URL 列表、重试次数)
    // 可以通过访问 assetsRetryStatistics[currentUrl] 来获取
    onSuccess: function(currentUrl) {
        console.log(currentUrl, assetsRetryStatistics[currentUrl])
    },
    onFail: function(currentUrl) {
        console.log(currentUrl, assetsRetryStatistics[currentUrl])
    }
})

当使用以上代码初始化完毕后,以下内容便获得了加载失败重试的能力:

  • 所有在 html 中使用 <script> 标签引用的脚本
  • 所有在 html 中使用 <link> 标签引用的样式 (跨域 CSS 需要正确配置
  • 所有在 html 中使用 <img> 标签引用的图片
  • 所有使用 document.createElement('script') 加载的脚本(如 webpack 的动态导入
  • 所有 css 中(包含同步与异步)使用的 background-image 图片

配置

assetsRetry 接受一个配置对象 AssetsRetryOptions ,其类型签名为:

interface AssetsRetryOptions {
    maxRetryCount: number
    onRetry: RetryFunction
    onSuccess: SuccessFunction
    onFail: FailFunction
    domain: Domain
}
type RetryFunction = (
    currentUrl: string,
    originalUrl: string,
    retryCollector: null | RetryStatistics
) => string | null
interface RetryStatistics {
    retryTimes: number
    succeeded: string[]
    failed: string[]
}
type SuccessFunction = (currentUrl: string) => void
type FailFunction = (currentUrl: string) => void
type Domain = string[] | { [x: string]: string }

具体说明如下:

  • domain: 域名列表,可配置为数组或对象类型
    • 数组类型:表示从域名列表中循环加载(1 -> 2 -> 3 -> ... -> n -> 1 -> ...),直到加载成功或超过限次
    • 对象类型:如 { 'a.cdn': 'b.cdn', 'c.cdn': 'd.cdn' } 表示在 a.cdn 失败的资源应从 b.cdn 重试,在 c.cdn 失败的资源应从 d.cdn 重试。
  • maxRetryCount: 每个资源的最大重试次数
  • onRetry: 在每次尝试重新加载资源时执行
    • 该函数接收 3 个参数:
      • currentUrl: 即将被选为重试地址的 URL
      • originalUrl: 上一次加载失败的 URL
      • retryCollector: 为当前资源的数据收集对象,如果资源为 CSS 中使用 url 引用的图片资源,该参数为 null 。当该参数不为 null 时,包含 3 个属性:
        • retryTimes: 表示当前为第 x 次重试(从 1 开始)
        • failed: 已失败的资源列表(从同一域名加载多次时,可能重复)
        • succeeded: 已成功的资源列表
    • 该函数的返回值必须为字符串或 null 对象。
      • 当返回 null 时,表示终止该次重试
      • 当返回字符串(url)时,会尝试从 url 中加载资源。
  • onSuccess: 在域名列表内的资源最终加载成功时执行:
    • currentUrl: 资源名,可通过该名称来找到当前资源的数据收集对象
  • onFail: 在域名列表内的资源最终加载失败时执行:
    • currentUrl: 资源名,可通过该名称来找到当前资源的数据收集对象

工作原理

Assets-Retry 的实现主要分为三部分:

  1. 如何自动获取加载失败的静态资源(同步加载的 <script>, <link>, <img>)并重试
  2. 如何自动获取加载失败的异步脚本并重试
  3. 如何自动获取加载失败的背景图片并重试

获取加载失败的静态资源

这部分实现较为简单,监听 document 对象的 error 事件便能够捕获到静态资源加载失败的错误。当 event.target 为需要重试的元素时,重试加载该元素即可。但需要注意以下场景:

<script src="/vendor.js"></script>
<script src="/app.js"></script>

在上面的代码中, app.js 依赖 vendor.js 中的功能,这在使用 webpack 打包的项目中极其常见,如果使用 document.createElement('script') 来对其进行重试,在网络环境不确定的情况下,app.js 很有可能比 vendor.js 先加载完毕,导致页面报错不可用。

所以对于在 html 中同步加载的 script 标签,在页面还未加载完毕时,需要使用 document.write,阻塞式地将 script 标签动态添加到 html 中。

获取加载失败的异步脚本

以 webpack 的动态加载脚本为例(仅保留关键代码):

function requireEnsure(chunkId) {
    // 加载 chunk 的 promise
    var promise = new Promise(function(resolve, reject) {
        installedChunkData = installedChunks[chunkId] = [resolve, reject]
    })
    installedChunkData[2] = promise
    // start chunk loading
    var script = document.createElement('script')
    var onScriptComplete
    script.charset = 'utf-8'
    script.timeout = 120
    script.src = jsonpScriptSrc(chunkId)
    onScriptComplete = function(event) {
        var chunk = installedChunks[chunkId]
        if (chunk) {
            // chunk[1] 加载 script 的 reject 回调
            chunk[1](new Error(/* ... */))
        }
    }
    script.onerror = script.onload = onScriptComplete
    document.head.appendChild(script)
}

在 webpack 等模块加载器中使用动态导入时, webpack 便会用上面的 requireEnsure 方法来保证对应的动态 chunk 被加载。如果某个 chunk 加载失败,则会进入 installChunkData[2] 中储存 Promise 的 reject 流程,而 Promise 一旦进入 rejected 状态,就再也无法改变到其他状态了。也就是说, webpack 并不会给我们重试的机会。

如何打破这种局面?摆在我们面前的只有两条路:

  1. 使用 webpack 插件,在编译期改写该段代码。
  2. 使用 monkey patch 对浏览器的原生方法进行改写。

为了降低集成成本,我们选择了第二种方案,即在运行时动态改写 document.createElement, Node#appendChild 等方法。在集成 Assets-Retry 后,调用 document.createElement API 并不会创建一个真正的 HTMLScriptElement ,取而代之的是一个 HookedScript 对象。并且,在 Node#appendChild 等方法中,如果检测到当前元素为 HookedScript 对象,则将appendChild 目标转换为其内部保存的真正的 HTMLScriptElement

增加这层代理后,我们就可以对 script 标签上的 onload, onerror 回调进行拦截,并进行重试处理。如果用户想设置对象的其他属性,如 src, type,则会设置到真正的 script 标签上,保证其他功能不受影响。

获取加载失败的背景图片

该部分通过 CSSStyleSheet 动态改变页面样式实现,当遇到图片类属性(如 background-image, border-image, list-style-image)时,自动添加一条包含备用域名的规则到网页样式中,浏览器便会自动发起重试,直到任一请求成功或均以失败告终。

浏览器兼容性

Chrome logo Edge logo Firefox logo Internet Explorer logo Opera logo Safari logo ios logo android logo
47+ ✔ 15+ ✔ 32+ ✔ 10+ ✔ 34+ ✔ 10+ ✔ 10+ ✔ 4.4+ ✔

常见问题

  1. Q: 为什么 CSS 或 CSS 中的背景图片无法从备用域名加载? A: 由于浏览器的安全策略,跨域的 CSS 默认无法动态获取 CSS 属性。修复方法:
    1. 加载跨域 CSS 的 link 标签上添加 crossorigin="anonymous" 属性。
    2. CDN 资源正确配置 Access-Control-Allow-Origin 头部

NPM scripts

  • npm t: Run test suite
  • npm start: Run npm run build in watch mode
  • npm run test:watch: Run test suite in interactive watch mode
  • npm run test:prod: Run linting and generate coverage
  • npm run build: Generate bundles and typings, create docs
  • npm run lint: Lints code
  • npm run commit: Commit using conventional commit style (husky will tell you to use it if you haven't 😉)

致谢

感谢 realworld.io 提供的 Demo App。