主题
脚本加载失败如何重试?
思路
需要考虑两个问题:
- 什么时候感知到脚本加载失败?
- 如何安全地重新发起加载?
先考虑第一个问题,什么时间点重试?
html
<!-- 为每个 script 监听 onerror 事件 -->
<body>
<script src="example1.js" onerror="handleError()"></script>
<script src="example2.js" onerror="handleError()"></script>
<script src="example3.js" onerror="handleError()"></script>
</body>html
<head>
<script>
// 统一监听 error
window.addEventListener(
'error',
(e) => {
// 过滤非脚本加载错误,保证后续的逻辑只针对于脚本加载错误
if (e instanceof ErrorEvent || e.target.tagName !== 'SCRIPT') {
return
}
// 处理重试逻辑 ...
},
true, // 由于脚本的 error 事件不冒泡,需要使用捕获阶段
)
</script>
</head>
<body>
<script src="example1.js"></script>
<script src="example2.js"></script>
<script src="example3.js"></script>
</body>由于现代前端工程通常会产出多个入口脚本、动态脚本和第三方脚本,所以不建议依赖方案 1。手动在每个 <script> 上都写 onerror,维护成本较高。
方案2 通过监听全局 error 事件来捕获所有脚本加载失败的情况,更加通用和优雅。需要注意的是,脚本的 error 事件不会冒泡,所以需要在捕获阶段监听。
再考虑第二个问题,如何重试?
- 准备一份备用域名列表
- 记录每个脚本当前尝试的备用域名索引
- 当脚本加载失败时,构造新的 URL 并重新插入脚本节点
注意事项
- 不建议使用
document.write()重新写入脚本。它会依赖文档写入时机,文档已完成解析后使用还有副作用。 - 重试时最好保留原脚本的重要属性,例如
type、crossorigin、referrerpolicy。 - 需要限制最大重试次数,避免无限循环。
- 如果脚本带有
integrity,更换源站时还要考虑 SRI 校验是否依然成立。
完整示例
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Script Retry</title>
<script>
const backupHosts = ['cdn-a.example.com', 'cdn-b.example.com', 'cdn-c.example.com']
const nextHostIndex = new Map()
function cloneScriptWithNewSrc(oldScript, newSrc) {
const newScript = document.createElement('script')
for (const { name, value } of oldScript.attributes) {
if (name !== 'src') {
newScript.setAttribute(name, value)
}
}
newScript.src = newSrc
return newScript
}
window.addEventListener(
'error',
(event) => {
if (event instanceof ErrorEvent || !(event.target instanceof HTMLScriptElement)) {
return
}
const failedScript = event.target
const url = new URL(failedScript.src)
const key = url.pathname
const currentIndex = nextHostIndex.get(key) ?? 0
if (currentIndex >= backupHosts.length) {
return
}
url.host = backupHosts[currentIndex]
nextHostIndex.set(key, currentIndex + 1)
const retryScript = cloneScriptWithNewSrc(failedScript, url.toString())
failedScript.replaceWith(retryScript)
},
true,
)
</script>
</head>
<body>
<script src="https://cdn.example.com/static/app.js" defer></script>
</body>
</html>