Umi 数据预加载功能详解

概述

Umi 提供了开箱即用的数据预加载方案,能够解决在多层嵌套路由下,页面组件和数据依赖的瀑布流请求问题。Umi 会自动根据当前路由或准备跳转的路由,并行地发起他们的数据请求,因此当路由组件加载完成后,已经有马上可以使用的数据了。

1. 传统方式的问题:瀑布流请求

什么是瀑布流请求(Waterfall Request)?

在传统的前端开发中(Vue 的 onMounted 或 React 的 useEffect),数据请求往往是在组件挂载后才发起的。在多层嵌套路由的场景下,这会导致"瀑布流"效应:

/dashboard
  └─ /dashboard/user
       └─ /dashboard/user/profile

传统方式的执行流程

// 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 🐌

问题分析

每层组件必须等待:

  1. 上一层数据加载完成
  2. 上一层组件渲染完成
  3. 才能开始自己的数据请求

这种串行的依赖关系形成了"瀑布"式的请求链,导致总加载时间是所有层级请求时间的累加。


2. Umi 数据预加载方案

并行请求机制

Umi 的 clientLoader 机制会在路由匹配阶段就并行发起所有相关组件的数据请求:

// 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 功能:

// .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 函数:

// 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 端点:

// 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' })
  }
}

在页面中使用:

// 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 传递:

// 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 错误处理

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 组件:

// .umirc.ts
export default defineConfig({
  clientLoader: {
    // 自定义 loading 组件
    loading: '@/components/Loading',
  },
});

7. 性能优化建议

7.1 合理使用缓存

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 避免过度预加载

不是所有数据都需要预加载,对于非关键数据可以在组件内按需加载:

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值