异步组件

2025/3/11 Vue3组件

# 1. 异步组件解决的问题

  1. 加载出错时要渲染的组件
  2. 指定 loading 组件,以及展示该组件的延迟时间
  3. 设置加载组件的超时时长
  4. 重试机制

加载与错误状态

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

  // 加载异步组件时使用的组件
  loadingComponent: LoadingComponent,
  // 展示加载组件前的延迟时间,默认为 200ms
  delay: 200,

  // 加载失败后展示的组件
  errorComponent: ErrorComponent,
  // 如果提供了一个 timeout 时间限制,并超时了
  // 也会显示这里配置的报错组件,默认值是:Infinity
  timeout: 3000
})

# 2. 异步组件的实现

异步组件的使用

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

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

<template>
  <AdminPage />
</template>

# 封装 defineAsyncComponent

defineAsyncComponent 用于定义一个异步组件,接受一个异步组件加载器作为参数, 返回值是一个包装组件

根据加载器的状态来决定返回的内容,加载完成则渲染该组件,否则渲染一个占位内容。

占位内容通常是一个注释节点,这里使用一个空文本节点。

先实现一个很糙的异步组件

function defineAsyncComponent(loader) {
  // 存储异步加载的组件
  let innerComp = null
  return {
    name: 'AsyncComponentWrapper',
    setup(){
      // 是否加载完成完成
      const loaded = ref(false)
      loader().then(c => {
        innerComp = c
        loaded.value = true
      })
      
      return () => {
        // 加载完成则渲染该组件,否则渲染一个占位内容
        return loaded.value ? { type: innerComp } : { type: 'Text', children: '' }
      }
    }
  }
}

# 超时 & Error 组件

既然有超时和其他清况,那我们就接收一个配置对象作为参数

  1. 设置一个变量来判断是否超时
  2. 组件开始加载,并且设置一个定时器,如果超过指定的时间,则认为超时。如果组件被卸载,则清除定时器。
  3. 根据是否加载完成和是否超时来决定具体的渲染内容,加载超时的时候,如果指定了错误组件,则渲染错误组件
  4. 当错误发生时,需要把错误对象传递出去,以便用户可以自行进行更细粒度的处理
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 timeout = ref(false)
      // 当错误发生时,用来存储错误信息
      const error = shallowRef(null)
      
      loader().then(c => {
        innerComp = c
        loaded.value = true
      }).catch(e => {
        // 通过 catch 来捕获加载过程中的初五
        error.value = e
      })
      
      let timer = null
      if(options.timeout){
        // 指定了超时时长,则开启一个定时器
        timer = setTimeout(() => {
          // timeout.value = true
          const err = new Error(`Async component timed out after ${options.timeout}`)
          error.value = err
        }, options.timeout)
      }
      
      // 组件卸载,清除定时器
      onUnmounted(() => clearTimeout(timer))
      
      // 占位内容
      const placeholder = {type: Text, children: ''}
      
      return () => {
        if(loaded.value){
          // 加载完成则渲染该组件
          return { type: innerComp }
        }
        
        // if(timeout.value){
        //   // 如果置指定了错误组件,则渲染该组件,否则渲染一个占位内容
        //   return options.errorComponent ? { type: options.errorComponent } : placeholder
        // }
        
        if(error.value && options.errorComponent){
          // 只有当错误存在,并且用户配置了 errorComponent 时,才渲染 Error 组件,同时把错误信息 error 作为 props 传递给 errorComponent 组件
          return { type: options.errorComponent, props: { error: error.value} }
        }
        
        return placeholder
      }
    }
  }
}

配置了错误信息,之前的变量 timeout 已经不再需要了。

# 延迟 & Loading 组件

使用:

  1. delay 指定延迟展示 loading 组件的时长
  2. loadingComponent 配置的 loading 组件

实现:

  1. 定义一个 loading 变量,标记是否正在加载
  2. 如果配置了 delay,则开启一个定时器,延时结束后设置 loading 为 true
  3. 无论是否加载完成都要清除定时器,否则可能出现组件已经加载成功,但是仍然展示 loading 组件
  4. 组件正在加载,并且用户配置了 loadingComponent,则渲染 loadingComponent
function defineAsyncComponent(options) {
  //...
  
  return {
    name: 'AsyncComponentWrapper',
    setup(){
      // 是否正在加载
      const loading = ref(false)
      
      const loadingTimer = null
      // 如果指定了延时,则开启一个定时器, 延时结束后设置 loading 为 true
      if(options.delay){
        loadingTimer = setTimeout(()=>{
          loading.value = true
        }, options.delay)
      }else{
        // 没有配置 delay,则直接设置 loading 为 true
        loading.value = true
      }
      
      loader().then(c => {
        innerComp = c
        loaded.value = true
      }).finally(() => {
        loading.value = true
        // 清除 loadingTimer
        clearTimeout(loadingTimer)
      })
      
      return () => {
        // 如果异步组件正在加载,并且用户配置了 loadingComponent,则渲染 loadingComponent 组件
        if(loaded.value && options.loadingComponent){
          return { type: options.loadingComponent }
        }
        // ...
      }
    }
  }
}

# 重试机制

function defineAsyncComponent(options) {
  //...
  
  // 记录重试次数
  let retries = 0
  
  // 通过 load 函数来加载异步组件
  function load(){
    return loader().catch(err => {
      if(options.onError){
        return new Promise((resolve, reject) => {
          // 重试
          const retry = () => {
            resolve()
            retries++
          }
          
          // 失败
          const fail = () => reject(err)
          // 把 retry,fail,retries 传递给用户自定义的 onError
          options.onError(retry, fail, retries)
        })
      }else{
        throw err
      }
    })
  }
  
  return {
    name: 'AsyncComponentWrapper',
    setup(){
      
      const error =  shallowRef(null)
      // ...
      load()
        .then(()=>{
          loaded.value = true
          innerComp = c
        })
        .catch((err)=>{
          error.value = err
      })
      
      // ...
    }
  }
}

源码 (opens new window)

Last Updated: 2026/06/09/ 06:29:26