13.2.4 重试机制
重试指的是加载出错时能重新发起加载请求:框架不替用户拍板,而是把
retry/fail两个函数通过onError回调交出去,由用户决定「再来一次」还是「就这么失败」。
一、捕获错误后的两种选择
- 捕获到错误时,要么直接抛出错误(走 13.2.2 的 Error 组件兜底);
- 要么返回一个新的 Promise 实例,并把该实例的
resolve/reject暴露给用户,让用户决定下一步怎么办。
由此得到用户侧的配置项:
onError(retry, fail, retries) {
if (retries < 3) {
retry() // 重新加载
} else {
fail() // 彻底失败
}
}
| 参数 | 含义 |
|---|---|
retry | 重新执行一次加载 |
fail | 放弃加载,让 Promise 进入 rejected 状态 |
retries | 当前已经重试的次数,用户可据此设置重试上限 |
二、用 load() 封装加载与重试
把加载逻辑从 setup 里抽出来,封装成 load(),重试时递归调用自己即可:
// 记录重试次数
let retries = 0
// 封装 load 函数用来加载异步组件
function load() {
return loader()
// 捕获加载器的错误
.catch(err => {
// 如果用户指定了 onError 回调,则将控制权交给用户
if (options.onError) {
// 返回一个新的 Promise 实例
return new Promise((resolve, reject) => {
// 重试:重新加载,并让外层的 then 接到新一次的结果
const retry = () => {
resolve(load())
retries++
}
// 失败:直接 reject,交给上层的 catch / Error 组件处理
const fail = () => reject(err)
// 作为参数传给用户,让用户来决定下一步怎么做
options.onError(retry, fail, retries)
})
} else {
// 用户没有提供 onError,直接抛出错误
throw err
}
})
}
retries定义在defineAsyncComponent的作用域内,而不是setup里,这样多次重试用的是同一个计数器;resolve(load())把新一次加载的 Promise 接力到外层的then/catch链上,所以setup里的写法完全不用改。
三、setup 里的使用
setup 中把原来的 loader() 换成 load()(其余逻辑与 13.2.3 一致):
// 原来调用的是 loader(),现在改为 load(),其余链式写法和 13.2.3 完全一致
load()
.then(c => {
InnerComp = c.default
loaded.value = true
})
.catch(err => {
error.value = err
clearTimeout(timer) // 已经出错,超时定时器就不再需要了
})
.finally(() => {
loading.value = false
clearTimeout(loadingTimer)
})
四、完整实现
function defineAsyncComponent(options) {
if (typeof options === 'function') {
options = { loader: options }
}
const { loader } = options
let InnerComp = null
// 记录重试次数
let retries = 0
// 封装 load 函数,把「加载 + 失败重试」收在一处
function load() {
return loader().catch(err => {
if (options.onError) {
return new Promise((resolve, reject) => {
const retry = () => {
resolve(load())
retries++
}
const fail = () => reject(err)
options.onError(retry, fail, retries)
})
} else {
throw err
}
})
}
return {
name: 'AsyncComponentWrapper',
setup() {
const loaded = ref(false)
const error = ref(null)
const loading = ref(false)
let loadingTimer = null
if (options.delay) {
loadingTimer = setTimeout(() => {
loading.value = true
}, options.delay)
} else {
loading.value = true
}
load() // 注意:这里调用的是 load 而不是 loader
.then(c => {
InnerComp = c.default
loaded.value = true
})
.catch(err => {
error.value = err
clearTimeout(timer)
})
.finally(() => {
loading.value = false
clearTimeout(loadingTimer)
})
let timer = null
if (options.timeout) {
timer = setTimeout(() => {
error.value = new Error(`Async component timed out after ${options.timeout}ms.`)
}, options.timeout)
}
onUnmounted(() => clearTimeout(timer))
const placeholder = { type: Text, children: '' }
return () => {
if (loaded.value) {
return { type: InnerComp }
} else if (error.value && options.errorComponent) {
return { type: options.errorComponent, props: { error: error.value } }
} else if (loading.value && options.loadingComponent) {
return { type: options.loadingComponent }
} else {
return placeholder
}
}
}
}
}
五、验证重试
让加载器第一次必定失败,即可看到重试被触发:
let testRetries = 0
const CompVNode = {
type: defineAsyncComponent({
loader: () => {
testRetries++
// 第一次直接 reject,模拟加载失败
return new Promise((resolve, reject) => {
testRetries === 1
? reject('error')
: setTimeout(() => resolve(import('../CompA.mjs')), 3000)
})
},
timeout: 2000,
errorComponent: MyErrorComp,
delay: 1000,
loadingComponent: {
setup() {
return () => ({ type: 'h5', children: 'Loading...' })
}
},
onError(retry, fail, retries) {
if (retries < 3) {
retry()
} else {
fail()
}
}
})
}
六、易错点
retry/fail必须二选一调用:两个都不调,返回的 Promise 永远不会 settle,界面会一直停在 Loading / 占位状态。
加载器要包成函数(
() => import(...)),Promise 只能消费一次,直接传 Promise 实例就没办法重试。
重试期间
error.value可能已经被超时逻辑赋过值,而渲染分支里error的判断在loading之前——想避免「重试时闪一下 Error 组件」,可在retry之前先把error.value置空。
出错时顺手
clearTimeout(timer)能省掉一次无意义的超时判定,避免超时错误覆盖掉真正的加载错误。
相关
- 13.3 函数式组件:另一种「没有实例」的组件形态,以及渲染器要配合做的改动