前端处理多个接口组合数据,核心目标是:可维护、可复用、错误可控、用户体验好。下面按"常见做法 → 最佳实践"来梳理。
一、常见场景分类
- 串行依赖:B 接口依赖 A 接口返回的某个 ID
- 并行无关:多个接口互不依赖,组合后渲染
- 部分失败容忍:某个接口失败,不影响其他数据展示
- 统一超时 / 取消:用户快速切换页面,避免请求"回写过期数据"
二、最佳实践方案(分层处理)
1. 数据请求层 ------ 用 Promise.all / Promise.allSettled
并行请求(推荐 90% 场景):
js
// 页面初始化:并行请求多个独立数据源
const fetchAllData = async () => {
const [userRes, goodsRes, couponRes] = await Promise.all([
api.getUser(),
api.getGoodsList(),
api.getCoupons()
]);
return {
user: userRes.data,
goodsList: goodsRes.data,
coupons: couponRes.data
};
};
部分失败容错 (推荐用 allSettled):
js
const [user, goods, coupon] = await Promise.allSettled([
api.getUser(),
api.getGoodsList(),
api.getCoupons()
]);
// 分别处理成功/失败状态
const userData = user.status === 'fulfilled' ? user.value : null;
2. 串行依赖 ------ 避免回调地狱,写清晰的 async/await
js
const fetchDataWithDeps = async () => {
// 1. 获取用户信息
const user = await api.getUser();
// 2. 用 user.id 获取订单列表
const orders = await api.getOrders(user.id);
// 3. 并行请求订单的详情(若有多个订单)
const detailPromises = orders.map(order => api.getOrderDetail(order.id));
const details = await Promise.all(detailPromises);
return { user, orders, details };
};
3. 状态管理组合 ------ 避免"巨型对象"
不要这样(一个 state 塞全部数据):
js
const [allData, setAllData] = useState({});
推荐:按数据模块拆分 state / atom
js
const [user, setUser] = useState(null);
const [goodsList, setGoodsList] = useState([]);
const [coupons, setCoupons] = useState([]);
const [loadingFlags, setLoadingFlags] = useState({
user: true,
goods: true,
coupon: true
});
或者用 React Query / SWR 自动管理 loading + error + 组合:
js
const { data: user } = useQuery(['user'], fetchUser);
const { data: goods } = useQuery(['goods'], fetchGoods);
const { data: coupon, error } = useQuery(['coupon'], fetchCoupon, {
// 该接口失败不影响页面主流程
throwOnError: false
});
// 组合数据
const pageData = useMemo(() => ({
user,
goods,
coupon: coupon ?? defaultCoupon
}), [user, goods, coupon]);
4. 性能优化:提前发起 + 去重 + 缓存
- 提前请求:hover 或路由切换时预加载
- 请求去重 :相同参数短时间内只发一次(
react-query自带) - 超时与取消:
js
// AbortController 配合 Promise.race 实现超时
const fetchWithTimeout = (url, timeout = 5000) => {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
return fetch(url, { signal: controller.signal })
.finally(() => clearTimeout(timeoutId));
};
5. UI 体验最佳实践
| 场景 | 推荐做法 |
|---|---|
| 全部接口必须成功才能展示页面 | 统一 loading,失败时整页重试 |
| 主要数据+次要数据(如推荐商品) | 主要数据 loading,次要数据静默加载 + 骨架屏 |
| 多个区块独立加载 | 按区块做 Suspense + 骨架屏,互不阻塞 |
| 用户频繁切换 tab/页面 | 使用 react-query 的 cancel 或 useEffect 清理,避免"先发后到"覆盖 |
三、真实项目推荐架构(React + React Query 示例)
js
// hooks/useDashboardData.js
export function useDashboardData(userId) {
const userQuery = useQuery(['user', userId], () => fetchUser(userId));
const postsQuery = useQuery(['posts', userId], () => fetchPosts(userId), {
enabled: !!userQuery.data // 依赖 user
});
const statsQuery = useQuery(['stats'], fetchStats, {
staleTime: 60_000, // 1分钟内不重新请求
retry: 1,
onError: (err) => console.warn('统计接口失败,不影响主流程')
});
const isLoading = userQuery.isLoading || postsQuery.isLoading;
const error = userQuery.error || postsQuery.error;
// 组合最终数据
const data = useMemo(() => {
if (!userQuery.data) return null;
return {
user: userQuery.data,
posts: postsQuery.data ?? [],
stats: statsQuery.data ?? { fallback: true }
};
}, [userQuery.data, postsQuery.data, statsQuery.data]);
return { data, isLoading, error };
}
四、总结:三条核心原则
- 并行用
Promise.all/allSettled,串行用async/await - 用数据请求库(React Query / SWR / Vue Query),而不是在组件里手写 loading + error + 组合逻辑
- 区分关键数据 vs 增强数据:关键接口失败要展示错误兜底,增强接口失败静默降级
这样既保证代码清晰,也让用户体验更顺滑,不会出现"一个无关接口失败,整个页面白屏"的情况。