13.2.2 超时与 Error 组件
加载超时和加载失败本质上都是「出错了」:只要统一成一个
error变量,就能用同一套渲染分支为它们兜底,并把错误对象作为 props 交给用户自定义的 Error 组件。
一、第一步:参数从加载器升级为配置对象
要让用户能配置超时与 Error 组件,defineAsyncComponent 就不能只接收加载器了,改为接收一个配置对象:
| 选项 | 作用 |
|---|---|
loader | 异步组件加载器,返回 Promise |
timeout | 超时时长,单位为 ms |
errorComponent | 出错(含超时)时要渲染的组件 |
为了兼容旧写法,函数入口先做一次归一化:
function defineAsyncComponent(options) {
// options 可以是加载器,也可以是配置对象
if (typeof options === 'function') {
options = { loader: options }
}
const { loader } = options
// ...
}
二、第二步:超时用标志变量标记(初版)
- 需要一个标志变量
timeout来标识异步组件的加载是否已经超时; - 指定了
timeout就开一个定时器,到点把timeout.value置为true; - 包装组件被卸载时要清除定时器,否则定时器会把状态写进一个已经废弃的组件实例;
- 渲染时根据
loaded与timeout两个变量决定渲染内容。
const loaded = ref(false)
const timeout = ref(false) // 代表是否超时
loader().then(c => {
InnerComp = c.default
loaded.value = true
})
let timer = null
if (options.timeout) {
timer = setTimeout(() => {
timeout.value = true
}, options.timeout)
}
// 包装组件被卸载时清除定时器
onUnmounted(() => clearTimeout(timer))
const placeholder = { type: Text, children: '' }
return () => {
if (loaded.value) {
// 加载成功,渲染被加载的组件
return { type: InnerComp }
} else if (timeout.value) {
// 加载超时,且用户配置了 Error 组件,则渲染该组件
return options.errorComponent ? { type: options.errorComponent } : placeholder
}
return placeholder
}
只用一个
timeout布尔值,表达不了「为什么出错」,也没法处理网络失败等其它错误。
三、第三步:统一成 error 对象,把控制权交给 Error 组件
为之前的实现做三处调整:
- 为加载器补上
catch,捕获所有加载错误(网络失败、模块不存在等); - 超时发生时,创建一个新的错误对象并赋值给
error.value——超时错误和其它错误从此走同一条路; - 渲染时只要
error.value存在且用户配置了errorComponent,就渲染它,并把error.value作为 props 传进去,让用户能做更细粒度的处理。
// 定义 error,当错误发生时用来存储错误对象
const error = ref(null)
loader()
.then(c => {
InnerComp = c.default
loaded.value = true
})
// 捕获加载过程中的错误
.catch(err => (error.value = err))
let timer = null
if (options.timeout) {
timer = setTimeout(() => {
// 超时后创建一个错误对象,同样写入 error
error.value = new Error(`Async component timed out after ${options.timeout}ms.`)
}, options.timeout)
}
onUnmounted(() => clearTimeout(timer))
四、完整实现
function defineAsyncComponent(options) {
// options 可以是加载器,也可以是配置对象
if (typeof options === 'function') {
options = { loader: options }
}
const { loader } = options
// 用来存储异步加载的组件
let InnerComp = null
return {
name: 'AsyncComponentWrapper',
setup() {
const loaded = ref(false)
// 当错误发生时,用来存储错误对象
const error = ref(null)
loader()
.then(c => {
InnerComp = c.default
loaded.value = true
})
.catch(err => (error.value = err))
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) {
// 只有错误存在且用户配置了 Error 组件时才展示,同时把 error 作为 props 传递
return { type: options.errorComponent, props: { error: error.value } }
}
return placeholder
}
}
}
}
五、Error 组件如何接收错误
在 Error 组件里用名为 error 的 props 接收错误对象,即可自行决定展示什么:
const MyErrorComp = {
props: {
error: String
},
render() {
return {
type: 'h3',
children: `error: ${this.error}`
}
}
}
const CompVNode = {
type: defineAsyncComponent({
// 模拟一个 3s 才加载完的组件
loader: () =>
new Promise(resolve => {
setTimeout(() => resolve(import('../CompA.mjs')), 3000)
}),
timeout: 1000, // 1s 后判定超时
errorComponent: MyErrorComp
})
}
renderer.render(CompVNode, document.querySelector('#app'))
六、易错点
超时只是人为造出来的一个错误:把它写进
error.value后,超时与网络错误的渲染路径完全一致,不需要两套判断。
定时器必须在包装组件卸载时
clearTimeout,否则组件早就被卸载了,定时器还在往废弃的实例上写状态。
渲染分支的顺序是
loaded→error→ 占位;顺序颠倒会出现「明明已经加载成功却还显示 Error 组件」。
Error 组件要拿到错误对象,必须声明同名的
props(如error),否则 props 解析不到、this.error是undefined。
相关
- 13.2.3 延迟与 Loading 组件:加载中该显示什么,以及如何避免 Loading 闪烁