Vue3 实战:多接口并发与界面渲染最佳实践

在 Vue3 中实现多接口并发请求,待所有接口返回后统一渲染页面,提升用户体验和加载效率。

在实际业务开发中,我们经常遇到这样的场景:进入一个页面后,需要同时请求多个独立的接口(如用户信息、商品列表、通知数据等),待所有接口返回后再统一渲染页面。 本文将通过一个电商首页的模拟场景,详解 Vue3 中多接口并发请求的实现方案,并对比不同方案的优劣。

场景模拟:电商首页数据加载

假设我们正在开发一个电商平台的首页,进入页面时需要同时请求三个接口:

  1. 首页轮播图数据 (/api/banners)
  2. 推荐商品列表 (/api/recommends)
  3. 用户未读消息数 (/api/notifications/count)

这三个接口彼此独立,无依赖关系,适合并发请求以提高加载效率。页面需要展示加载状态,全部请求完成后隐藏加载动画并渲染数据,若任一请求失败则显示错误提示。

方案实现与代码示例

方案一:基于 axios + Promise.all 实现

步骤 1:封装 axios 实例

首先创建 src/utils/request.js 封装请求基础配置:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import axios from 'axios';
import { ElMessage } from 'element-plus'; // 假设使用 Element Plus 组件库

const request = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL, // 从环境变量获取基础地址
  timeout: 8000, // 超时时间 8s
  headers: {
    'Content-Type': 'application/json'
  }
});

// 请求拦截器:添加 Token
request.interceptors.request.use(
  (config) => {
    const token = localStorage.getItem('token');
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
  },
  (error) => {
    ElMessage.error('请求配置错误');
    return Promise.reject(error);
  }
);

// 响应拦截器:处理错误
request.interceptors.response.use(
  (response) => response.data, // 直接返回数据体
  (error) => {
    const message = error.response?.data?.msg || '请求失败,请稍后重试';
    ElMessage.error(message);
    return Promise.reject(error); // 保留错误供调用方处理
  }
);

export default request;

步骤 2:创建 API 函数

src/api/home.js 中定义接口请求函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import request from '@/utils/request';

// 获取轮播图数据
export const getBanners = () => {
  return request.get('/banners');
};

// 获取推荐商品
export const getRecommends = () => {
  return request.get('/recommends', {
    params: { limit: 8 } // 限制返回8条数据
  });
};

// 获取未读消息数
export const getUnreadCount = () => {
  return request.get('/notifications/count');
};

步骤 3:页面组件实现(核心)

在首页组件中实现并发请求与渲染逻辑:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
<template>
  <div class="home-page">
    <!-- 加载状态 -->
    <div class="loading-mask" v-if="loading">
      <el-spinner size="50" />
      <p>数据加载中...</p>
    </div>

    <!-- 错误提示 -->
    <div class="error提示" v-if="error">
      <el-icon color="red"><warning /></el-icon>
      <p>数据加载失败,请刷新页面重试</p>
    </div>

    <!-- 内容区域(全部请求成功后显示) -->
    <template v-if="!loading && !error">
      <!-- 轮播图 -->
      <el-carousel v-if="banners.length" height="300px">
        <el-carousel-item v-for="banner in banners" :key="banner.id">
          <img :src="banner.imgUrl" alt="轮播图" class="banner-img" />
        </el-carousel-item>
      </el-carousel>

      <!-- 推荐商品 -->
      <div class="recommends" v-if="recommends.length">
        <h2>为您推荐</h2>
        <el-row :gutter="20">
          <el-col :span="6" v-for="item in recommends" :key="item.id">
            <el-card :body-style="{ padding: '10px' }">
              <img :src="item.imgUrl" alt="商品图片" class="product-img" />
              <h3 class="product-name">{{ item.name }}</h3>
              <p class="product-price">¥{{ item.price.toFixed(2) }}</p>
            </el-card>
          </el-col>
        </el-row>
      </div>

      <!-- 消息提示 -->
      <el-badge 
        v-if="unreadCount > 0" 
        :value="unreadCount" 
        class="message-badge"
      >
        <el-button icon="message" circle />
      </el-badge>
    </template>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue';
import { getBanners, getRecommends, getUnreadCount } from '@/api/home';
import { Warning } from '@element-plus/icons-vue';

// 状态管理
const loading = ref(true); // 加载状态
const error = ref(false); // 错误状态
const banners = ref([]); // 轮播图数据
const recommends = ref([]); // 推荐商品
const unreadCount = ref(0); // 未读消息数

// 页面挂载时请求数据
onMounted(async () => {
  try {
    // 并发请求三个接口
    const [bannerData, recommendData, countData] = await Promise.all([
      getBanners(),
      getRecommends(),
      getUnreadCount()
    ]);

    // 赋值到响应式变量
    banners.value = bannerData.list;
    recommends.value = recommendData.items;
    unreadCount.value = countData.unread;
  } catch (err) {
    // 任一请求失败则标记错误状态
    error.value = true;
    console.error('数据加载失败:', err);
  } finally {
    // 无论成功失败,都结束加载状态
    loading.value = false;
  }
});
</script>

<style scoped>
.loading-mask {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(255, 255, 255, 0.8);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  z-index: 999;
}

.banner-img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.recommends {
  margin: 20px;
}

.product-img {
  width: 100%;
  height: 180px;
  object-fit: cover;
}

.product-name {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  margin: 10px 0;
}

.product-price {
  color: #ff4d4f;
  font-weight: bold;
}

.message-badge {
  position: fixed;
  top: 20px;
  right: 20px;
}
</style>

方案二:使用 vue-request 简化状态管理

如果项目中需要频繁处理请求状态(loading / error / data),可以使用 vue-request 进一步简化代码:

安装依赖

1
npm install vue-request

组件实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
<template>
  <!-- 加载状态任一请求未完成则显示 -->
  <div class="loading-mask" v-if="loading">
    <el-spinner size="50" />
  </div>

  <!-- 错误提示任一请求失败则显示 -->
  <div class="error提示" v-if="error">
    <el-icon color="red"><warning /></el-icon>
    <p>数据加载失败</p>
  </div>

  <!-- 内容区域 -->
  <template v-if="!loading && !error">
    <!-- 轮播图复用之前的结构 -->
    <el-carousel v-if="bannerResult.list?.length" height="300px">
      <!-- ...轮播图内容... -->
    </el-carousel>

    <!-- 推荐商品复用之前的结构 -->
    <div class="recommends" v-if="recommendResult.items?.length">
      <!-- ...商品列表内容... -->
    </div>
  </template>
</template>

<script setup>
import { useRequest } from 'vue-request';
import { getBanners, getRecommends, getUnreadCount } from '@/api/home';

// 轮播图请求
const { 
  data: bannerResult, 
  loading: bannerLoading, 
  error: bannerError 
} = useRequest(getBanners);

// 推荐商品请求
const { 
  data: recommendResult, 
  loading: recommendLoading, 
  error: recommendError 
} = useRequest(getRecommends);

// 未读消息请求
const { 
  data: countResult, 
  loading: countLoading, 
  error: countError 
} = useRequest(getUnreadCount);

// 合并状态:任一请求加载中则整体加载中
const loading = computed(() => bannerLoading.value || recommendLoading.value || countLoading.value);
// 合并状态:任一请求失败则整体错误
const error = computed(() => bannerError.value || recommendError.value || countError.value);
</script>

多接口并发请求的注意事项

错误处理策略

  • Promise.all 具有“快速失败”特性:任一请求失败会立即触发 catch,适合需要所有接口都成功的场景
  • 若允许部分接口失败(如非核心数据),可使用 Promise.allSettled:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    const results = await Promise.allSettled([
      getBanners(), // 轮播图
      getRecommends(), // 推荐商品
      getUnreadCount() // 未读消息数
    ]);
    // 过滤成功的结果
    const successData = results
      .filter(r => r.status === 'fulfilled')
      .map(r => r.value);
    

请求中断

页面跳转时需中断未完成的请求,避免无效请求浪费资源:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { onUnmounted } from 'vue';
import axios from 'axios';

// 创建取消令牌
const source = axios.CancelToken.source();

onMounted(async () => {
  try {
    const [bannerData, recommendData] = await Promise.all([
      request.get('/banners', { cancelToken: source.token }),
      request.get('/recommends', { cancelToken: source.token })
    ]);
  } catch (err) {
    if (axios.isCancel(err)) {
      console.log('请求已取消:', err.message);
    }
  }
});

// 组件卸载时取消请求
onUnmounted(() => {
  source.cancel('页面已跳转,取消请求');
});

性能优化

  • 接口返回数据尽量精简,避免传输冗余字段
  • 对非实时数据添加缓存(如 vue-requestcacheTime 配置)
  • 合理设置超时时间(避免过长阻塞页面,过短导致频繁失败)

方案对比

方案 优点 缺点 适用场景
axios + Promise.all 灵活可控,无额外依赖 需手动管理状态 大多数中大型项目
vue-request 自动管理状态,减少模板代码 增加依赖体积 频繁处理请求状态的场景
fetch + Promise.all 原生支持,无依赖 需手动处理错误和 JSON 转换 轻量项目,无复杂需求

实际开发中 axios + Promise.all 是最均衡的选择,既满足大部分业务需求,又不会引入过多依赖。对于状态管理复杂的页面(如数据仪表盘),vue-request 能显著减少重复代码。

如果本文对您有所帮助,欢迎打赏支持作者!

Licensed under CC BY-NC-SA 4.0
最后更新于 2025-10-28 10:10
使用 Hugo 构建
主题 StackJimmy 设计