Umi 数据预加载功能详解

概述

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 🐌

问题分析

每层组件必须等待:

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

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


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

相关推荐
Zzzzzxl_2 天前
互联网大厂前端面试实录:HTML5、ES6、Vue/React、工程化与性能优化全覆盖
性能优化·vue·es6·react·html5·前端面试·前端工程化
老李说技术3 天前
React 中 useCallback 的基本使用和原理解析
react
csj503 天前
前端基础之《React(7)—webpack简介-ESLint集成》
前端·react
老李说技术4 天前
React中useContext的基本使用和原理解析
react
w2sfot4 天前
如何将React自定义语法转化为标准JavaScript语法?
javascript·react
rengang665 天前
502-Spring AI Alibaba React Agent 功能完整案例
人工智能·spring·agent·react·spring ai·ai应用编程
csj506 天前
前端基础之《React(6)—webpack简介-图片模块处理》
前端·react
!win !6 天前
从一个按钮实例入门CSS in JS之styled-components
css·react
明仔的阳光午后8 天前
React 入门 01:快速写一个React的HelloWorld项目
前端·javascript·react.js·前端框架·reactjs·react