异步组件和 <suspense>

在大型应用的 Vue 中,可能会有成百上千个组件,但是对于单个用户来说,所访问的页面不需要用到所有组件的代码,可能只需要其中一部分,或者说在当前页面不需要,而在下一个页面需要,所以我们需要有一种机制能够做到只在需要的时候才去加载一个组件。这些组件需要在一些异步的逻辑判断之后才能加载,称这些 组件为异步组件Vue 有一个 defineAsyncComponent 方法可以创建异步组件,如示例代码 3-5-1 所示。

示例代码3-5-1 异步组件
<div id="app">
    <async-example/>
</div>

<script type="text/javascript">
    const app = Vue.createApp({})
    const AsyncComp = Vue.defineAsyncComponent(
        () => new Promise((resolve,reject) => {
            setTimeout(() => {
                resolve({
                    template: '<div>I am async!</div>'
                })
            }, 3000)
        })
    )
    app.component('async-example', AsyncComp)
    app.mount("#app")
</script>

上面的代码运行后,在页面中 3s 后会出现 <async-example> 组件的内容,模拟了需要异步操作的场景。defineAsyncComponent 方法也可以接收一个对象,提供更加丰富的配置,代码如下:

const AsyncComp = Vue.defineAsyncComponent(
    {
        // 工厂函数
        loader: () => import('./Foo.vue'),
        // 加载异步组件时要使用的组件
        loadingComponent: LoadingComponent,
        // 加载失败时要使用的组件
        errorComponent: ErrorComponent,
        // 在显示 loadingComponent 之前的延迟 | 默认值:200(单位为 ms)
        delay: 200,
        // 如果提供了 timeout,并且加载组件的时间超过了设定值,将显示错误指令
        timeout: 3000,
        // 定义组件是否可挂起 | 默认值: true
        suspensible: false,

        /**
         *
         * @param error 错误信息对象
         * @param retry 一个函数,用于指示当 promise 加载器 reject 时,加载器是否应该重试
         * @param fail 一个函数,指示加载程序结束退出
         * @param attempts 允许的最大重试次数
         */
        onError(error, retry, fail, attempts) {
            if (error.message.match(/fetch/) && attempts <= 3) {
                // 请求发生错误时重试,最多可尝试3次
                retry()
            } else {
                // 注意,retry/fail 就像 promise 的 resolve/reject 一样
                // 必须调用其中一个才能继续错误处理
                fail()
            }
        }
    }
)

在等待异步结果时,页面展示空白总是体验不太好,这时就可以借助 Vue 3 中的 <suspense>,如示例代码 3-5-2 所示。

<div id="app">
    <suspense>
        <template #default>
            <async-example />
        </template>
        <template #fallback>
            <div>
                Loading...
            </div>
        </template>
    </suspense>
</div>

<script type="text/javascript">
    const app = Vue.createApp({})
    const AsyncComp = Vue.defineAsyncComponent(
        () => new Promise((resolve,reject) => {
            setTimeout(() => {
                resolve({
                    template: '<div>I am async!</div>'
                })
            }, 3000)
        })
    )
    app.component('async-example', AsyncComp)
    app.mount("#app")
</script>

上面的代码中,在 <async-example> 组件加载之前,页面会首先展示 Loading…​,以此来提升等待时的用户体验。

<suspense> 组件有两个插槽,它们都只接收一个子组件。default 插槽里的内容会优先展示,前提是里面的内容被全部解析,而如果是异步组件,则需要异步逻辑执行完成之后才能解析,这时先展示 fallback 插槽里的内容。

需要说明的是,default 插槽里的 <async-example> 可以是 异步组件也可以本身不是异步组件,但是其子组件是异步组件,这种情况下也需要所有子组件的异步逻辑全部执行完之后才会完成解析<suspense> 也可以和动态组件结合使用,例如 Vue Router 中的 <router-view> 和动画组件 <transition> 等,如示例代码 3-5-3 所示。

<router-view v-slot="{ Component }">
    <template v-if="Component">
        <keep-alive>
            <suspense>
                <component :is="Component"></component>
                <template #fallback>
                    <div>
                        Loading...
                    </div>
                </template>
            </suspense>
        </keep-alive>
    </template>
</router-view>

上面代码中的场景是,采用 Vue Router 切换页面时,添加过渡动画,在动画的间隙展示 Loading…​ 来提升用户体验,我们会在后面的章节详细讲解 Vue Router 和动画的使用。