Vue3-异步组件 && suspense

115 阅读4分钟

异步组件(Async Components)也称为懒加载组件(Lazy-loaded Components)。

默认情况下,Webpack 或 Vite 等构建工具会把你所有的组件打包到一个(或几个)JavaScript 文件中。当用户访问你的网站时,他们必须一次性下载所有代码,这可能导致初始加载时间很长。

异步组件允许你将某些组件(通常是“重量级”或不总是需要立即显示的组件)分离成单独的 JavaScript 文件(chunk) 。这些文件只会在实际需要渲染该组件时才从服务器下载。

主要好处:

  • 代码分割 (Code-splitting): 减小初始包的体积。
  • 提升性能: 加快应用的初始加载和渲染速度。

1.在 Vue 3 中, 使用 defineAsyncComponent 函数来创建异步组件

举个栗子,骨架屏,准备两个组件,card和skeleton

card.vue

<template>
  <div class="card">
    <div class="user-info">
      <img :src="user.avatar" alt="avatar" />
      <div class="user-name">{{ user.name }}</div>
    </div>
    <hr />
    <div class="content">
      <p>{{ user.content }}</p>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";
interface User {
  id: number;
  name: string;
  avatar: string;
  content: string;
}

const user = ref<User>({
  id: 1,
  name: "YaeZed",
  avatar: "https://avatars.githubusercontent.com/u/52018740?v=5",
  content: "Hello, Vue3!",
});
</script>

<style scoped>
.card {
  width: 300px;
  height: 300px;
  box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.3);
}

.user-info {
  display: flex;
  flex-direction: row;
  text-align: center;
  align-items: center;
}

img {
  width: 150px;
  height: 150px;
  border-radius: 100px;
}

.user-name {
  margin-left: 30px;
  font-size: 24px;
  font-weight: bold;
}

.content {
  margin-left: 10px;
}
</style>

skeleton.vue

<template>
  <div class="card">
    <div class="user-info">
      <img src="" alt="" />
      <div class="user-name"></div>
    </div>
    <hr />
    <div class="content">
      <p></p>
    </div>
  </div>
</template>

<script setup lang="ts"></script>

<style scoped>
.card {
  width: 300px;
  height: 300px;
  box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.3);
}

.user-info {
  display: flex;
  flex-direction: row;
  text-align: center;
  align-items: center;
}

img {
  background-color: bisque;
  width: 150px;
  height: 150px;
  border-radius: 100px;
}

.user-name {
  width: 70px;
  height: 20px;
  background-color: bisque;
  margin-left: 30px;
  font-size: 24px;
  font-weight: bold;
}

.content {
  background-color: beige;
  height: 120px;
  width: 300px;
}
</style>

结合使用异步组件和 Suspense

App.vue中 ,使用 Suspense 来加载 card.vue(作为异步组件),并在加载时显示 skeleton.vue

<template>
 <div>
   <h1>应用</h1>
   <p>下面的卡片是异步加载的...</p>

   <Suspense>

     <template #default>
       <AsyncUserCard />
     </template>

     <template #fallback>
       <CardSkeleton />
     </template>

   </Suspense>
 </div>
</template>

<script setup lang="ts">
// 导入 defineAsyncComponent 来创建异步组件
import { defineAsyncComponent } from 'vue';

// 1. 同步导入骨架屏
//
// "fallback" 内容必须是立即(同步)可用的,
// 所以我们像平常一样导入 skeleton.vue。
import CardSkeleton from './skeleton.vue';

// 2. 异步导入你的卡片组件
//
// 我们使用 `defineAsyncComponent` 来“包装”你的 card.vue。
// 这告诉 Vue 这是一个异步组件,应该懒加载。
const AsyncUserCard = defineAsyncComponent(() => 
 import('./card.vue')
);


// ----------------------------------------------------
// 💡 (可选) 如何在本地测试骨架屏:
//
// 在本地开发中,`card.vue` 加载得太快,你可能
// 看不到骨架屏。你可以像这样模拟一个 2 秒的网络延迟:
//
// const AsyncUserCard = defineAsyncComponent(() => {
//   return new Promise(resolve => {
//     setTimeout(() => {
//       // @ts-ignore
//       resolve(import('./card.vue'));
//     }, 2000); // 延迟 2 秒
//   });
// });
// ----------------------------------------------------

</script>

<style>
/* 一些全局样式 */
body {
 font-family: sans-serif;
 padding: 20px;
}
</style>

流程

  • 用户加载 App.vue

  • Suspense 开始渲染。它尝试渲染 #default 插槽。

  • 它发现 #default 里的 AsyncUserCard 是一个异步组件,并且尚未加载。

  • Suspense 立即切换到渲染 #fallback 插槽,显示 CardSkeleton

  • 同时,Vue 在后台开始下载 card.vue 对应的 JavaScript 文件。

  • 下载和解析完成后,Suspense 自动用 AsyncUserCard 的内容替换掉 CardSkeleton

2.另一种触发 Suspense 的方式:async setup

修改一下card.vue

<template>
  <div class="card">
    <div class="user-info">
      <img :src="user.avatar" alt="avatar" />
      <div class="user-name">{{ user.name }}</div>
    </div>
    <hr />
    <div class="content">
      <p>{{ user.content }}</p>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";
interface User {
  id: number;
  name: string;
  avatar: string;
  content: string;
}

// 1. 模拟一个异步数据获取 (例如 API 调用)
const fetchUserData = (): Promise<User> => {
  return new Promise(resolve => {
    setTimeout(() => {
      console.log("数据获取完成!");
      resolve({
        id: 1,
        name: "YaeZed (来自 API)",
        avatar: "https://avatars.githubusercontent.com/u/52018740?v=4",
        content: "Hello, from async setup!",
      });
    }, 2000); // 模拟 2 秒的 API 调用
  });
};

// 2. 在 <script setup> 顶层使用 await
//
// Vue 会自动将 `setup` 变为 `async setup`。
// `Suspense` 将会等待这个 `await` (fetchUserData) 完成。
const user = ref<User>(await fetchUserData());

</script>

<style scoped>
/* 样式不变 */
.card {
  width: 300px;
  height: 300px;
  box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.3);
}

.user-info {
  display: flex;
  flex-direction: row;
  text-align: center;
  align-items: center;
}

img {
  width: 150px;
  height: 150px;
  border-radius: 100px;
}

.user-name {
  margin-left: 30px;
  font-size: 24px;
  font-weight: bold;
}

.content {
  margin-left: 10px;
}
</style>

现在,card.vue 自身包含了一个异步操作。App.vue 可以这样写:

<template>
  <Suspense>
    <template #default>
      <UserCard />
    </template>
    <template #fallback>
      <CardSkeleton />
    </template>
  </Suspense>
</template>

<script setup lang="ts">
// 这次可以同步导入 card
import UserCard from './card.vue'; 
import CardSkeleton from './skeleton.vue';
</script>

在这个版本中,同步导入UserCard,但因为 UserCard 内部有 async setupSuspense 仍然会捕获这个异步状态,并在 await fetchUserData() 完成之前显示 CardSkeleton

3.处理加载失败

异步组件在网络不稳定或资源不存在时可能会加载失败。为了防止整个页面崩溃,通常会配合 Vue 的 onErrorCaptured 钩子或专门的“错误边界”组件来处理异常。

可以通过 defineAsyncComponent 的高级配置项来处理超时或加载失败的情况:

const AsyncUserCard = defineAsyncComponent({
  loader: () => import('./card.vue'),
  // 加载异步组件时使用的组件(类似 skeleton)
  loadingComponent: CardSkeleton,
  // 展示加载组件前的延迟时间。默认:200ms
  delay: 200,
  // 如果提供了一个加载组件,在超时前它将被展示
  timeout: 3000,
  // 加载失败时使用的组件
  errorComponent: ErrorComponent, 
});

或者在 Suspense 的父组件中使用 onErrorCaptured 钩子来捕获异步依赖抛出的任何错误。

参考文章

小满zs 学习Vue3 第十八章(异步组件&代码分包&suspense)xiaoman.blog.csdn.net/article/det…