{"id":13672794,"url":"https://github.com/Nikaple/assets-retry","last_synced_at":"2025-04-28T03:33:00.760Z","repository":{"id":41176229,"uuid":"225188662","full_name":"Nikaple/assets-retry","owner":"Nikaple","description":":repeat: Non-intrusive assets retry implementation. 无侵入式的静态资源自动重试","archived":false,"fork":false,"pushed_at":"2024-01-09T04:38:36.000Z","size":3261,"stargazers_count":314,"open_issues_count":8,"forks_count":59,"subscribers_count":6,"default_branch":"master","last_synced_at":"2025-04-16T22:51:40.275Z","etag":null,"topics":["assets-ensure","assets-retry","cdn-ensure","cdn-failover","cdn-retry","css-retry","ensure-bundle","fallback","js-ensure","js-failover","js-retry"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Nikaple.png","metadata":{"files":{"readme":"README-cn.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"code-of-conduct.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2019-12-01T16:01:43.000Z","updated_at":"2025-03-19T13:13:53.000Z","dependencies_parsed_at":"2023-02-05T20:46:30.045Z","dependency_job_id":"2798f184-bd2f-437e-b0bb-ac50f98b26d5","html_url":"https://github.com/Nikaple/assets-retry","commit_stats":{"total_commits":97,"total_committers":7,"mean_commits":"13.857142857142858","dds":0.4742268041237113,"last_synced_commit":"7b0c0b8edd873f2448b1da3cb1a9872a3b4bb736"},"previous_names":[],"tags_count":13,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Nikaple%2Fassets-retry","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Nikaple%2Fassets-retry/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Nikaple%2Fassets-retry/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Nikaple%2Fassets-retry/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Nikaple","download_url":"https://codeload.github.com/Nikaple/assets-retry/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":251153817,"owners_count":21544391,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["assets-ensure","assets-retry","cdn-ensure","cdn-failover","cdn-retry","css-retry","ensure-bundle","fallback","js-ensure","js-failover","js-retry"],"created_at":"2024-08-02T09:01:48.680Z","updated_at":"2025-04-28T03:32:55.748Z","avatar_url":"https://github.com/Nikaple.png","language":"TypeScript","funding_links":[],"categories":["TypeScript"],"sub_categories":[],"readme":"[English](./README.md) | 简体中文\n\n# 静态资源自动重试\n\n当页面中的脚本、样式、图片资源无法正常加载时，自动重试加载失败的资源。支持备用域名、动态导入（dynamic import），无需改动现有代码，仅需 3 KB （gzipped）。\n\n![Demo GIF](./public/assets-retry.gif)\n\n### [Demo 地址](https://nikaple.com/assets-retry/vue/)\n\n## 目录\n\n-   [安装](#安装)\n-   [快速上手](#快速上手)\n-   [配置](#配置)\n-   [工作原理](#工作原理)\n    -   [获取加载失败的静态资源](#获取加载失败的静态资源)\n    -   [获取加载失败的异步脚本](#获取加载失败的异步脚本)\n    -   [获取加载失败的静态资源](#获取加载失败的静态资源)\n-   [常见问题](#常见问题)\n\n### 安装\n\n#### 通过 npm 安装\n\n```bash\n$ npm install assets-retry --save\n```\n\n然后通过 [webpack 配置](./examples/webpack) 内联到页面的 `head` 标签中，并置于**所有资源开始加载之前**。\n\n#### 直接通过 `script` 标签引用\n\n如果你懒得折腾 webpack 配置，可以将 [assets-retry.umd.js](https://github.com/Nikaple/assets-retry/blob/master/dist/assets-retry.umd.js) 直接内联到 `\u003chead\u003e` 标签中，并置于**所有资源开始加载之前**。\n\n### 快速上手\n\n使用起来非常简单，只需要初始化并传入域名列表即可：\n\n```js\n// assetsRetryStatistics 中包含所有资源重试的相关信息\nvar assetsRetryStatistics = window.assetsRetry({\n    // 域名列表，只有在域名列表中的资源，才会被重试\n    // 使用以下配置，当 https://your.first.domain/js/1.js 加载失败时\n    // 会自动使用 https://your.second.domain/namespace/js/1.js 重试\n    domain: ['your.first.domain', 'your.second.domain/namespace'],\n    // 可选，最大重试次数，默认 3 次\n    maxRetryCount: 3,\n    // 可选，通过该参数可自定义 URL 的转换方式\n    onRetry: function(currentUrl, originalUrl, statistics) {\n        return currentUrl\n    },\n    // 对于给定资源，要么调用 onSuccess ，要么调用 onFail，标识其最终的加载状态\n    // 加载详细信息（成功的 URL、失败的 URL 列表、重试次数）\n    // 可以通过访问 assetsRetryStatistics[currentUrl] 来获取\n    onSuccess: function(currentUrl) {\n        console.log(currentUrl, assetsRetryStatistics[currentUrl])\n    },\n    onFail: function(currentUrl) {\n        console.log(currentUrl, assetsRetryStatistics[currentUrl])\n    }\n})\n```\n\n当使用以上代码初始化完毕后，以下内容便获得了加载失败重试的能力：\n\n-   [x] 所有在 `html` 中使用 `\u003cscript\u003e` 标签引用的脚本\n-   [x] 所有在 `html` 中使用 `\u003clink\u003e` 标签引用的样式 （跨域 CSS 需要正确[配置](#常见问题)）\n-   [x] 所有在 `html` 中使用 `\u003cimg\u003e` 标签引用的图片\n-   [x] 所有使用 `document.createElement('script')` 加载的脚本（如 webpack 的[动态导入](https://webpack.docschina.org/guides/code-splitting/#%E5%8A%A8%E6%80%81%E5%AF%BC%E5%85%A5-dynamic-imports-)）\n-   [x] 所有 `css` 中（包含同步与异步）使用的 `background-image` 图片\n\n### 配置\n\n`assetsRetry` 接受一个配置对象 `AssetsRetryOptions` ，其类型签名为：\n\n```ts\ninterface AssetsRetryOptions {\n    maxRetryCount: number\n    onRetry: RetryFunction\n    onSuccess: SuccessFunction\n    onFail: FailFunction\n    domain: Domain\n}\ntype RetryFunction = (\n    currentUrl: string,\n    originalUrl: string,\n    retryCollector: null | RetryStatistics\n) =\u003e string | null\ninterface RetryStatistics {\n    retryTimes: number\n    succeeded: string[]\n    failed: string[]\n}\ntype SuccessFunction = (currentUrl: string) =\u003e void\ntype FailFunction = (currentUrl: string) =\u003e void\ntype Domain = string[] | { [x: string]: string }\n```\n\n具体说明如下：\n\n-   `domain`: 域名列表，可配置为数组或对象类型\n    -   数组类型：表示从域名列表中循环加载（1 -\u003e 2 -\u003e 3 -\u003e ... -\u003e n -\u003e 1 -\u003e ...），直到加载成功或超过限次\n    -   对象类型：如 `{ 'a.cdn': 'b.cdn', 'c.cdn': 'd.cdn' }` 表示在 `a.cdn` 失败的资源应从 `b.cdn` 重试，在 `c.cdn` 失败的资源应从 `d.cdn` 重试。\n-   `maxRetryCount`: 每个资源的最大重试次数\n-   `onRetry`: 在每次尝试重新加载资源时执行\n    -   该函数接收 3 个参数：\n        -   `currentUrl`: 即将被选为重试地址的 `URL`\n        -   `originalUrl`: 上一次加载失败的 `URL`\n        -   `retryCollector`: 为当前资源的数据收集对象，如果资源为 CSS 中使用 `url` 引用的图片资源，**该参数为 `null`** 。当该参数不为 `null` 时，包含 3 个属性：\n            -   `retryTimes`: 表示当前为第 x 次重试（从 1 开始）\n            -   `failed`: 已失败的资源列表（从同一域名加载多次时，可能重复）\n            -   `succeeded`: 已成功的资源列表\n    -   该函数的返回值必须为字符串或 `null` 对象。\n        -   当返回 `null` 时，表示终止该次重试\n        -   当返回字符串（url）时，会尝试从 url 中加载资源。\n-   `onSuccess`: 在域名列表内的资源最终加载成功时执行：\n    -   `currentUrl`: 资源名，可通过该名称来找到当前资源的数据收集对象\n-   `onFail`: 在域名列表内的资源最终加载失败时执行：\n    -   `currentUrl`: 资源名，可通过该名称来找到当前资源的数据收集对象\n\n### 工作原理\n\nAssets-Retry 的实现主要分为三部分：\n\n1. 如何自动获取加载失败的静态资源（同步加载的 `\u003cscript\u003e`, `\u003clink\u003e`, `\u003cimg\u003e`）并重试\n2. 如何自动获取加载失败的异步脚本并重试\n3. 如何自动获取加载失败的背景图片并重试\n\n#### 获取加载失败的静态资源\n\n这部分实现较为简单，监听 `document` 对象的 `error` 事件便能够捕获到静态资源加载失败的错误。当 `event.target` 为需要重试的元素时，重试加载该元素即可。但需要注意以下场景：\n\n```html\n\u003cscript src=\"/vendor.js\"\u003e\u003c/script\u003e\n\u003cscript src=\"/app.js\"\u003e\u003c/script\u003e\n```\n\n在上面的代码中， `app.js` 依赖 `vendor.js` 中的功能，这在使用 webpack 打包的项目中极其常见，如果使用 `document.createElement('script')` 来对其进行重试，在网络环境不确定的情况下，`app.js` 很有可能比 `vendor.js` 先加载完毕，导致页面报错不可用。\n\n所以对于在 `html` 中同步加载的 `script` 标签，在页面还未加载完毕时，需要使用 `document.write`，阻塞式地将 `script` 标签动态添加到 `html` 中。\n\n#### 获取加载失败的异步脚本\n\n以 webpack 的动态加载脚本为例（仅保留关键代码）：\n\n```javascript\nfunction requireEnsure(chunkId) {\n    // 加载 chunk 的 promise\n    var promise = new Promise(function(resolve, reject) {\n        installedChunkData = installedChunks[chunkId] = [resolve, reject]\n    })\n    installedChunkData[2] = promise\n    // start chunk loading\n    var script = document.createElement('script')\n    var onScriptComplete\n    script.charset = 'utf-8'\n    script.timeout = 120\n    script.src = jsonpScriptSrc(chunkId)\n    onScriptComplete = function(event) {\n        var chunk = installedChunks[chunkId]\n        if (chunk) {\n            // chunk[1] 加载 script 的 reject 回调\n            chunk[1](new Error(/* ... */))\n        }\n    }\n    script.onerror = script.onload = onScriptComplete\n    document.head.appendChild(script)\n}\n```\n\n在 webpack 等模块加载器中使用动态导入时， webpack 便会用上面的 `requireEnsure` 方法来保证对应的动态 chunk 被加载。如果某个 chunk 加载失败，则会进入 `installChunkData[2]` 中储存 Promise 的 reject 流程，而 Promise 一旦进入 rejected 状态，就再也无法改变到其他状态了。也就是说， webpack 并不会给我们重试的机会。\n\n如何打破这种局面？摆在我们面前的只有两条路：\n\n1. 使用 webpack 插件，在编译期改写该段代码。\n2. 使用 [monkey patch](https://en.wikipedia.org/wiki/Monkey_patch) 对浏览器的原生方法进行改写。\n\n为了降低集成成本，我们选择了第二种方案，即在运行时动态改写 `document.createElement`, `Node#appendChild` 等方法。在集成 `Assets-Retry` 后，调用 `document.createElement` API 并不会创建一个真正的 `HTMLScriptElement` ，取而代之的是一个 `HookedScript` 对象。并且，在 `Node#appendChild` 等方法中，如果检测到当前元素为 `HookedScript` 对象，则将`appendChild` 目标转换为其内部保存的真正的 `HTMLScriptElement` 。\n\n增加这层代理后，我们就可以对 `script` 标签上的 `onload`, `onerror` 回调进行拦截，并进行重试处理。如果用户想设置对象的其他属性，如 `src`, `type`，则会设置到真正的 `script` 标签上，保证其他功能不受影响。\n\n#### 获取加载失败的背景图片\n\n该部分通过 [CSSStyleSheet](https://developer.mozilla.org/zh-CN/docs/Web/API/CSSStyleSheet) 动态改变页面样式实现，当遇到图片类属性（如 `background-image`, `border-image`, `list-style-image`）时，自动添加一条包含备用域名的规则到网页样式中，浏览器便会自动发起重试，直到任一请求成功或均以失败告终。\n\n### 浏览器兼容性\n\n| \u003cimg src=\"./public/chrome.png\" width=\"48px\" height=\"48px\" alt=\"Chrome logo\"\u003e | \u003cimg src=\"./public/edge.png\" width=\"48px\" height=\"48px\" alt=\"Edge logo\"\u003e | \u003cimg src=\"./public/firefox.png\" width=\"48px\" height=\"48px\" alt=\"Firefox logo\"\u003e | \u003cimg src=\"./public/ie.png\" width=\"48px\" height=\"48px\" alt=\"Internet Explorer logo\"\u003e | \u003cimg src=\"./public/opera.png\" width=\"48px\" height=\"48px\" alt=\"Opera logo\"\u003e | \u003cimg src=\"./public/safari.png\" width=\"48px\" height=\"48px\" alt=\"Safari logo\"\u003e | \u003cimg src=\"./public/ios.png\" height=\"48px\" alt=\"ios logo\"\u003e | \u003cimg src=\"./public/android.svg\" width=\"48px\" height=\"48px\" alt=\"android logo\"\u003e |\n| :--------------------------------------------------------------------------: | :----------------------------------------------------------------------: | :----------------------------------------------------------------------------: | :---------------------------------------------------------------------------------: | :------------------------------------------------------------------------: | :--------------------------------------------------------------------------: | :-------------------------------------------------------: | :----------------------------------------------------------------------------: |\n|                                    47+ ✔                                     |                                  15+ ✔                                   |                                     32+ ✔                                      |                                        10+ ✔                                        |                                   34+ ✔                                    |                                    10+ ✔                                     |                           10+ ✔                           |                                     4.4+ ✔                                     |\n\n### 常见问题\n\n1. Q: 为什么 CSS 或 CSS 中的背景图片无法从备用域名加载？\n   A: 由于浏览器的安全策略，跨域的 CSS 默认无法动态获取 CSS 属性。修复方法：\n    1. 加载跨域 CSS 的 link 标签上添加 `crossorigin=\"anonymous\"` 属性。\n    2. CDN 资源正确配置 [Access-Control-Allow-Origin](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin) 头部\n\n### NPM scripts\n\n-   `npm t`: Run test suite\n-   `npm start`: Run `npm run build` in watch mode\n-   `npm run test:watch`: Run test suite in [interactive watch mode](http://facebook.github.io/jest/docs/cli.html#watch)\n-   `npm run test:prod`: Run linting and generate coverage\n-   `npm run build`: Generate bundles and typings, create docs\n-   `npm run lint`: Lints code\n-   `npm run commit`: Commit using conventional commit style ([husky](https://github.com/typicode/husky) will tell you to use it if you haven't :wink:)\n\n### 致谢\n\n感谢 [realworld.io](https://realworld.io) 提供的 Demo App。\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FNikaple%2Fassets-retry","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FNikaple%2Fassets-retry","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FNikaple%2Fassets-retry/lists"}