脚本加载失败如何重试
在项目中遇到的一个难点,无论是我们用的原生 js 还是用的框架最后上线的时候都会是打包好之后,打包好之后的 js 文件中都会自己或者自动引入 script,在生产环境中会出现其中有一个 script 无法加载成功的时候怎么处理,当 js 加载不出来的时候页面是显示不出来的,最起码功能是不正常的,现在都是单页面应用,js 加载不成功的话对页面影响还是比较大的,所以我们需要去处理这个问题。。。
- 什么时间重试 捕获
- 如何重试
1. 加载失败后立即重试(无延迟)
适用场景:临时网络波动导致的瞬时失败,适合对时效性要求高的非核心脚本。
实现方式:在 script.onerror 事件触发时直接发起重试。
javascript
// 在 head 标签中添加 script 元素
const backupDomains = [
'https://www.baidu.com',
'https://www.google.com',
'https://www.bing.com',
'https://www.yahoo.com',
'https://www.youtube.com',
'https://www.facebook.com',
'https://www.twitter.com',
'https://www.instagram.com',
'https://www.tiktok.com'
]
// 存储每个路径的当前尝试索引和已使用过的域名
const domainState = {}
function getNextDomain(pathname) {
// 初始化状态
if (!domainState[pathname]) {
domainState[pathname] = {
index: 0,
used: new Set()
}
}
const state = domainState[pathname]
// 寻找下一个未使用过的域名
while (state.index < backupDomains.length) {
const domain = backupDomains[state.index]
state.index++
if (!state.used.has(domain)) {
state.used.add(domain)
return domain
}
}
// 所有备用域名都已尝试
return null
}
function replaceScript(originalScript, newUrl) {
// 创建新脚本元素
const newScript = document.createElement('script')
// 复制原脚本的属性
Array.from(originalScript.attributes).forEach((attr) => {
newScript.setAttribute(attr.name, attr.value)
})
// 设置新的URL
newScript.src = newUrl
// 插入新脚本并移除原脚本
originalScript.parentNode.insertBefore(newScript, originalScript)
originalScript.remove()
return newScript
}
window.addEventListener(
'error',
(e) => {
// 只处理脚本加载错误
if (!(e.target instanceof HTMLScriptElement) || !e.target.src) {
return
}
try {
const originalUrl = new URL(e.target.src)
const pathname = originalUrl.pathname
// 获取下一个备用域名
const nextDomain = getNextDomain(pathname)
if (!nextDomain) {
console.error(`所有备用域名都已尝试,脚本 ${pathname} 加载失败`)
return
}
// 构建新的URL
originalUrl.hostname = nextDomain
const newUrl = originalUrl.toString()
console.log(`脚本 ${e.target.src} 加载失败,尝试备用域名: ${newUrl}`)
// 替换脚本
replaceScript(e.target, newUrl)
} catch (error) {
console.error('处理脚本加载错误时发生异常:', error)
}
},
true
)2. 延迟重试(固定间隔)
适用场景:可能因服务器负载过高导致的失败,通过延迟减少服务器压力。
实现方式:使用 setTimeout 设置固定间隔(如 1 秒)后重试。
javascript
function loadScriptWithDelay(url, retries = 3, delay = 1000) {
const script = document.createElement('script')
script.src = url
script.onerror = () => {
if (retries > 0) {
console.log(`将在 ${delay}ms 后重试加载 ${url}`)
script.remove()
setTimeout(() => {
loadScriptWithDelay(url, retries - 1, delay)
}, delay)
}
}
document.head.appendChild(script)
}3. 指数退避重试(推荐)
适用场景:网络不稳定或服务器响应缓慢的情况,避免短时间内频繁请求。
实现方式:每次重试的间隔时间按指数增长(如 1s → 2s → 4s)。
javascript
function loadScriptWithBackoff(url, retries = 3, baseDelay = 1000) {
const script = document.createElement('script')
script.src = url
script.onerror = () => {
if (retries > 0) {
const delay = baseDelay * Math.pow(2, 3 - retries) // 指数增长
console.log(`将在 ${delay}ms 后重试加载 ${url}`)
script.remove()
setTimeout(() => {
loadScriptWithBackoff(url, retries - 1, baseDelay)
}, delay)
}
}
document.head.appendChild(script)
}4. 页面空闲时重试
适用场景:非紧急脚本(如统计、广告),避免影响核心功能加载。
实现方式:利用 requestIdleCallback 在浏览器空闲时重试。
javascript
function loadScriptWhenIdle(url, retries = 3) {
const script = document.createElement('script')
script.src = url
script.onerror = () => {
if (retries > 0) {
console.log(`等待浏览器空闲后重试加载 ${url}`)
script.remove()
// 浏览器空闲时重试(超时时间5000ms)
requestIdleCallback(
() => {
loadScriptWhenIdle(url, retries - 1)
},
{ timeout: 5000 }
)
}
}
document.head.appendChild(script)
}5. 用户交互时重试
适用场景:依赖用户操作的脚本(如点击按钮后才需要的功能)。
实现方式:在用户触发特定事件(如 click、scroll)时重试。
javascript
function loadScriptOnInteraction(url, retries = 3) {
let script
function attemptLoad() {
script = document.createElement('script')
script.src = url
script.onerror = () => {
if (retries > 0) {
retries--
script.remove()
console.log(`等待用户交互后重试加载 ${url}`)
}
}
document.head.appendChild(script)
}
// 首次尝试加载
attemptLoad()
// 用户点击时重试(仅在还有重试次数时)
document.addEventListener(
'click',
() => {
if (retries > 0 && !script?.isConnected) {
attemptLoad()
}
},
{ once: true }
) // 只监听一次点击
}