基本用法

在大型项目中,经常需要拆分应用为更小的块,并仅在需要时再从服务器加载相关组件。Vue提供了defineAsyncComponent方法来实现此功能:

import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent(() => {
return new Promise((resolve, reject) => {
//...从服务器获取组件
resolve(/* 获取到的组件 */)
})
})

// ...像使用其他组件一样使用 `AsyncComp`

defineAsyncComponent 方法接收一个返回Promise的加载函数。这个Promise的resolve回调方法应该在从服务器获得组件定义时调用。也可以调用reject(reason)表明加载失败。

ES模块的动态导入也会返回一个Promise,所以多数情况下会将其与defineAsyncComponent搭配使用。类似vite和webpack这样的构建工具也支持这样的语法,因此可以用它来导入Vue单文件组件:

import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent(() => import('./components/MyComponent.vue'))

最后得到的AsyncComp是一个外层包装过的组件,仅在页面需要它渲染时才会调用加载内部实际组件的函数。它会将接收到的props和插槽传给内部组件,所以可以使用异步的包装组件无缝替换原始组件,同时实现延迟加载。

与普通组件一样,异步组件可以使用app.component()全局注册:

app.component('MyComponent', defineAsyncComponent(() => import('./components/MyComponent.vue')))

也可以直接在父组件中直接定义它们:

<script setup>
import { defineAsyncComponent } from 'vue'

const AdminPage = defineAsyncComponent(() => import('./components/MyComponent.vue'))
</script>

<template>
<AdminPage />
</template>

加载与错误状态

异步操作不可避免会涉及到加载和错误状态,因此defineAsyncComponent()也支持在高级选项中处理这些状态:

const AsyncComp = defineAsyncComponent({
//加载函数
loader: () => import('./Foo.vue'),

//加载异步组件时使用的组件
loadingComponent: LoadingComponent,

//展示加载组件前的延迟时间,默认为200ms
delay: 200,

//加载失败后展示的组件
errorComponent: ErrorComponent,

//如果提供一个timeout时间限制并超时,也会使用加载失败组件,timeout默认值为:Infinity
timeout: 3000
})

如果提供了一个加载组件,它将在内部组件加载时先行显示。在加载组件显示之前有一个默认的200ms延迟–这是因为在网络状况比较好的时候,加载完成的很快,加载组件和最终组件之间的替换太快可能产生闪烁,反而影响感受。

如果提供了一个报错组件,则它会在加载器函数返回的Promise报错时被渲染。还可以指定一个超时时间,在请求耗时超过指定时间时也会渲染报错组件。

搭配Suspense组件使用

<Suspense>是一个内置组件,用来在组件树中协调对异步依赖的处理。使用它可以在组件树上层等待下层的多个嵌套异步依赖项解析完成,并可以在等待时渲染一个加载状态。

异步依赖

假设现在有这样一种组件层级结构:

<Suspense>
|___<Dashboard>
|_____<Profile>
| |____<FriendStatus> (组件有异步的setup())
|
|_____<Content>
|____<ActivityFeed> (异步组件)
|____<Stats> (异步组件)

在这个组件树中有多个嵌套组件,要渲染它们,首先得解析一些异步资源。如果没有<Suspense>,则它们每个都需要处理自己的加载、报错和完成状态,在最坏的情况下,可能会在页面上看到三个旋转的加载态,在不同的时间显示出内容。

有了<Suspense>组件后,就可以在等待整个多层级组件树中的各个异步依赖获取结果时,在顶层展示出加载中或加载失败的状态。

<Suspense>可以等待的异步依赖有两种:

  1. 带有异步setup()钩子的组件。也包含了使用<script setup>时有顶层await表达式的组件。
  2. 异步组件。

async setup()

组合式API中组件的setup()钩子可以是异步的:

export default {
async setup() {
const res = await fetch(...)
const posts = await res.json()

return {
posts
}
}
}

如果使用的是<script setup>,那么顶层await表达式会自动让该组件成为个异步依赖:

<script setup>
const res = await fetch(...)
const posts = await res.json()
</script>

<template>
{{ posts }}
</template>

异步组件

异步组件默认就是suspensible的,这意味着如果组件关系链上有一个<Suspense>,那么这个异步组件就会被当做是这个<Suspense>的一个异步依赖。在这种情况下,加载状态是由<Suspense>控制,而该组件自己的加载、报错、延时和超时等选项都将被忽略。

异步组件也可以通过在选项中指定suspensible: false表明不用Suspense控制,并让组件始终自己控制其加载状态。

加载中状态

<Suspense>组件有两个插槽:#default#fallback。这两个插槽都只允许一个直接子节点。在可能的时候都将显示默认插槽中的节点。否则将显示后备插槽中的节点。

<Suspense>
<!-- 具有深层异步依赖的组件 -->
<Dashboard />

<!-- 在#fallback 插槽中显示 "正在加载中" -->
<template #fallback>
Loading...
</template>
</Suspense>

在初始渲染时,<Suspense>将在内存中渲染其默认的插槽内容。如果在这个过程中遇到任何异步依赖,则会进入挂起状态。在挂起状态期间,展示的是后备内容。当所有遇到的异步依赖都完成后,<Suspense>会进入完成状态,并将展示出默认插槽的内容。

如果在初次渲染时没有遇到异步依赖,<Suspense>会直接进入完成状态。

进入完成状态后,只有当默认插槽的根节点被替换时,<Suspense>才会回到挂起状态。组件树中新的更深层次的异步依赖不会造成<Suspense>回退到挂起状态。

发生回退时,后备内容不会立即展示出来。相反<Suspense>在等待新内容和异步依赖完成时,会展示之前#default插槽的内容。这个行为可以通过一个timeoutprop进行配置: 在等待渲染新内容耗时超过timeout之后,<Suspense>将会切换为展示后备内容。若timeout值为0将导致在替换默认内容时立即显示后备内容。

事件

<Suspense>组件会触发三个事件:pendingresolvefallbackpending事件是在进入挂起状态时触发。resolve事件是在default插槽完成获取新内容时触发。fallback事件则是在fallback插槽的内容显示时触发。

例如,可以使用这些事件在加载新组件时在之前的DOM最上层显示一个加载指示器。

错误处理

<Suspense>组件自身还不提供错误处理,可以使用errorCaptured选项或者onErrorCaptured()钩子,在使用到<Suspense>的父组件中捕获和处理异步错误。

和其他组件结合

通常在使用过程中,会将<Suspense><Transition><KeepAlive>等组件结合。要保证这些组件都能正常工作,嵌套的顺序非常重要。

另外,这些组件都通常与VueRouter的<RouterView>组件结合使用。

下面的示例展示了如何嵌套这些组件,使它们都能按照预期的方式运行。

<RouterView v-slot="{ Component }">
<template v-if="Component">
<Transition mode="out-in">
<KeepAlive>
<Suspense>
<!-- 主要内容 -->
<component :is="Component"></component>

<!-- 加载中状态 -->
<template #fallback>
正在加载...
</template>
</Suspense>
</KeepAlive>
</Transition>
</template>
</RouterView>

VueRouter使用动态导入对懒加载组件进行了内置支持。这些与异步组件不同,目前他们不会触发<Suspense>。但是它们仍然可以有异步组件作为后代,这些组件可以照常触发<Suspense>

嵌套使用

当有多个类似与下方的异步组件时:

<Suspense>
<component :is="DynamicAsyncOuter">
<component :is="DynamicAsyncInner"/>
</component>
</Suspense>

<Suspense>创建了一个边界,它将如预期的那样解析树下的所有异步组件。然而当更改DynamicAsyncOuter时,<Suspense>会正确地等待它,但当更改DynamicAsyncInner时,嵌套的DynamicAsyncInner会呈现为一个空节点,直到它被解析为止(而不是之前的节点或者回退插槽)。

为了解决这个问题,可以使用嵌套的方法来处理嵌套组件:

<Suspense>
<component :is="DynamicAsyncOuter">
<Suspense suspensible>
<compoent :is="DynamicAsyncInner" />
</Suspense>
</compoent>
</Suspense>

如果不设置suspensible属性,内部的<Suspense>将被父级<Suspense>视为同步组件。这意味着它将有自己的回退插槽,如果两个Dynamic组件同时被修改,则当子<Suspense>加载其自己的依赖关系树时,可能会出现空节点和多个修补周期,这可能不是理想情况。设置后,所有异步依赖项处理都会交给父级<Suspense>(包括发出的事件),而内部<Suspense>仅充当依赖项解析和修补的另一个边界。