概述
Umi 提供了开箱即用的数据预加载方案,能够解决在多层嵌套路由下,页面组件和数据依赖的瀑布流请求问题。Umi 会自动根据当前路由或准备跳转的路由,并行地发起他们的数据请求,因此当路由组件加载完成后,已经有马上可以使用的数据了。
1. 传统方式的问题:瀑布流请求
什么是瀑布流请求(Waterfall Request)?
在传统的前端开发中(Vue 的 onMounted 或 React 的 useEffect),数据请求往往是在组件挂载后才发起的。在多层嵌套路由的场景下,这会导致"瀑布流"效应:
/dashboard
└─ /dashboard/user
└─ /dashboard/user/profile
传统方式的执行流程
javascript
// Vue 示例
// 步骤1: 加载 Dashboard 组件
onMounted(async () => {
await fetchDashboardData() // 等待 200ms
// Dashboard 渲染完成
})
// 步骤2: 加载 User 组件(Dashboard 渲染后才开始)
onMounted(async () => {
await fetchUserData() // 等待 150ms
// User 渲染完成
})
// 步骤3: 加载 Profile 组件(User 渲染后才开始)
onMounted(async () => {
await fetchProfileData() // 等待 180ms
// Profile 渲染完成
})
// 总耗时:200ms + 150ms + 180ms = 530ms 🐌
问题分析
每层组件必须等待:
- 上一层数据加载完成
- 上一层组件渲染完成
- 才能开始自己的数据请求
这种串行的依赖关系形成了"瀑布"式的请求链,导致总加载时间是所有层级请求时间的累加。
2. Umi 数据预加载方案
并行请求机制
Umi 的 clientLoader 机制会在路由匹配阶段就并行发起所有相关组件的数据请求:
typescript
// src/pages/Dashboard/index.tsx
export async function clientLoader() {
return await fetchDashboardData(); // 同时发起!
}
// src/pages/Dashboard/User/index.tsx
export async function clientLoader() {
return await fetchUserData(); // 同时发起!
}
// src/pages/Dashboard/User/Profile/index.tsx
export async function clientLoader() {
return await fetchProfileData(); // 同时发起!
}
// 总耗时:max(200ms, 150ms, 180ms) = 200ms ⚡
// 提速:530ms → 200ms(快了 62%)
3. 核心区别对比
| 特性 | 传统方式(onMounted/useEffect) | Umi 数据预加载 |
|---|---|---|
| 请求时机 | 组件渲染后 | 路由匹配后立即发起 |
| 执行顺序 | 串行(瀑布流) | 并行 |
| 总耗时 | 累加所有层级 | 取最慢的一个 |
| 用户体验 | 逐层加载,多次白屏 | 一次性加载完成 |
| 代码位置 | 组件内部 | 独立的 loader 函数 |
| Loading 状态 | 需要手动管理 | 框架自动处理 |
4. 实际使用示例
4.1 配置路由
首先在 .umirc.ts 中开启 clientLoader 功能:
typescript
// .umirc.ts
import { defineConfig } from '@umijs/max';
export default defineConfig({
clientLoader: {}, // 开启客户端数据加载
routes: [
{
path: '/dashboard',
component: './Dashboard',
routes: [
{
path: '/dashboard/user/:id',
component: './Dashboard/User',
},
],
},
],
npmClient: 'pnpm',
});
4.2 定义 Loader
在页面组件中导出 clientLoader 函数:
typescript
// src/pages/Dashboard/User/index.tsx
import { useClientLoaderData } from '@umijs/max';
// 📌 导出 clientLoader,Umi 会在路由匹配时自动调用
export async function clientLoader({ params }: any) {
const res = await fetch(`/api/user/${params.id}`);
return await res.json();
}
export default function UserPage() {
// 直接使用预加载的数据,无需 loading 状态
const data = useClientLoaderData();
return (
<div className="p-6">
<h1 className="text-2xl font-bold">{data.name}</h1>
<p className="text-gray-600">{data.email}</p>
</div>
);
}
4.3 结合 Umi API 路由
Umi 支持约定式 API 路由,可以在 src/api 目录下创建 API 端点:
typescript
// src/api/foo.ts
import type { UmiApiRequest, UmiApiResponse } from "umi";
export default async function (req: UmiApiRequest, res: UmiApiResponse) {
switch (req.method) {
case 'GET':
res.json({ "foo": "is working", "timestamp": Date.now() })
break;
default:
res.status(405).json({ error: 'Method not allowed' })
}
}
在页面中使用:
typescript
// src/pages/foo/index.tsx
import { useClientLoaderData } from '@umijs/max';
export async function clientLoader() {
// 调用约定式 API 路由
const res = await fetch('/api/foo');
return await res.json();
}
export default function FooPage() {
const data = useClientLoaderData();
return (
<div className="flex items-center justify-center min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
<div className="p-8 bg-white rounded-2xl shadow-xl">
<h1 className="text-3xl font-bold text-indigo-600 mb-4">
Foo API Response
</h1>
<pre className="bg-gray-100 p-4 rounded-lg text-sm">
{JSON.stringify(data, null, 2)}
</pre>
</div>
</div>
);
}
5. 形象比喻:瀑布 vs 并行
瀑布流(传统方式)
水从上往下流,每一层必须等上一层流下来
🏔️ 父组件挂载 → 请求数据 (200ms)
↓
💧 数据返回 → 渲染
↓
🏔️ 子组件挂载 → 请求数据 (150ms)
↓
💧 数据返回 → 渲染
↓
🏔️ 孙组件挂载 → 请求数据 (180ms)
并行请求(Umi 方式)
所有水同时从各层流出
🏔️🏔️🏔️ 所有组件同时请求
💧💧💧 等最慢的返回后一起渲染
6. 进阶用法
6.1 依赖父级数据
有时子组件需要依赖父组件的数据,可以通过 params 传递:
typescript
// src/pages/Dashboard/index.tsx
export async function clientLoader() {
const dashboard = await fetchDashboard();
return { dashboardId: dashboard.id };
}
// src/pages/Dashboard/User/index.tsx
export async function clientLoader({ params, matches }: any) {
// 获取父级 loader 的数据
const parentData = matches[0].data;
const res = await fetch(`/api/user?dashboardId=${parentData.dashboardId}`);
return await res.json();
}
6.2 错误处理
typescript
export async function clientLoader() {
try {
const res = await fetch('/api/data');
if (!res.ok) throw new Error('请求失败');
return await res.json();
} catch (error) {
return { error: error.message };
}
}
export default function Page() {
const data = useClientLoaderData();
if (data.error) {
return <div className="text-red-500">加载失败:{data.error}</div>;
}
return <div>{/* 正常渲染 */}</div>;
}
6.3 Loading 状态
Umi 提供了全局的导航进度条,也可以自定义 loading 组件:
typescript
// .umirc.ts
export default defineConfig({
clientLoader: {
// 自定义 loading 组件
loading: '@/components/Loading',
},
});
7. 性能优化建议
7.1 合理使用缓存
typescript
export async function clientLoader({ request }: any) {
const cache = await caches.open('my-cache');
const cached = await cache.match(request.url);
if (cached) {
return await cached.json();
}
const res = await fetch(request.url);
cache.put(request.url, res.clone());
return await res.json();
}
7.2 避免过度预加载
不是所有数据都需要预加载,对于非关键数据可以在组件内按需加载:
typescript
export default function Page() {
const criticalData = useClientLoaderData(); // 预加载的关键数据
const [optionalData, setOptionalData] = useState(null);
useEffect(() => {
// 非关键数据按需加载
fetchOptionalData().then(setOptionalData);
}, []);
return <div>{/* ... */}</div>;
}
8. 总结
核心优势
- ⚡ 性能提升显著:在多层路由场景下,总加载时间从累加变为取最大值
- 🎯 数据和路由解耦 :
clientLoader作为独立函数,更易维护和测试 - 🚀 更好的用户体验:减少白屏时间,一次性加载完成后再渲染
- 🛠️ 开箱即用:无需额外配置复杂的状态管理,Umi 自动处理
适用场景
- ✅ 多层嵌套路由
- ✅ 需要 SEO 的页面(配合 SSR)
- ✅ 数据依赖关系复杂的应用
- ✅ 需要优化首屏加载的项目
不适用场景
- ❌ 简单的单页应用
- ❌ 实时性要求极高的数据(建议使用 WebSocket)
- ❌ 数据量特别大需要分页加载的场景
参考资料
文章作者 : 写完这行代码打球去
创建时间 : 2025年11月5日
技术栈: Umi 4 + React + TypeScript