React Router 双重加载器机制:服务端 loader 与客户端 clientLoader 完整解析

using both loaders 这套机制是 React Router (以及类似现代框架) 的精髓所在,它巧妙地平衡了服务器渲染(SSR)的速度和客户端渲染(CSR)的灵活性。

我们可以把整个主题分成两个部分来理解:

  1. 默认模式loader 负责首屏,clientLoader 负责后续导航。
  2. 增强模式 :使用 clientLoader.hydrate = true 强制 clientLoader 在首屏加载时也运行。

第一部分:默认模式(解释第一个代码块)

jsx 复制代码
// route("products/:pid", "./product.tsx");
import type { Route } from "./+types/product";
import { fakeDb } from "../db";

// 1. 服务端加载器
export async function loader({ params }: Route.LoaderArgs) {
  // 直接从数据库或内部模块获取数据
  return fakeDb.getProduct(params.pid);
}

// 2. 客户端加载器
export async function clientLoader({
  serverLoader,
  params,
}: Route.ClientLoaderArgs) {
  // 通过 fetch 调用一个公开的 API
  const res = await fetch(`/api/products/${params.pid}`);
  // 调用 serverLoader() 来复用 loader 的数据
  const serverData = await serverLoader();
  // 合并两边的数据
  return { ...serverData, ...(await res.json()) };
}

export default function Product({
  loaderData,
}: Route.ComponentProps) {
  /* ... */
}

这段代码展示了 loaderclientLoader 如何协同工作,它们各自在最适合自己的环境中发挥作用。

场景 A:用户首次访问页面 (Initial Load / SSR)

当用户在浏览器地址栏输入 http://your-site.com/products/123 并回车时:

  1. 请求到达服务器:服务器识别出这个 URL 匹配产品详情页的路由。

  2. loader 函数运行

    • 环境 :在服务器上运行。
    • 目的:为页面的首次渲染(SSR)提供核心数据。
    • 执行 :它直接调用 fakeDb.getProduct('123')。因为在服务器内部,它可以安全、快速地直接访问数据库、文件系统或内部模块(这里是 fakeDb),无需通过网络请求。
    • 返回 :一个包含产品数据的对象,例如 { id: '123', name: '超酷键盘' }
  3. clientLoader 函数不运行 :在这次初始加载中,clientLoader 被完全忽略

  4. 服务器渲染 HTML :服务器使用 loader 返回的数据渲染 <Product> 组件,生成一个完整的 HTML 页面,然后发送给浏览器。

  5. 用户体验 :用户几乎立刻就能看到一个包含产品名称和描述的完整页面,实现了极快的首屏加载速度(FCP/LCP)

用户已经在网站上,现在他从首页点击了一个链接,跳转到 /products/456

  1. 请求在客户端被拦截:React Router 阻止了浏览器重新加载整个页面,而是启动了客户端导航。

  2. loader 函数不运行:它在服务器上的任务已经完成了。

  3. clientLoader 函数运行

    • 环境 :在用户的浏览器中运行。

    • 目的:为客户端的页面更新获取数据。

    • 执行

      • 它发起一个 fetch 请求到 /api/products/456。为什么不直接访问数据库?因为浏览器无法直接访问服务器的数据库,它必须通过一个公开的 API 端点来请求数据。这是一种更安全、更规范的做法。
      • 神奇的 serverLoader() :它调用了 serverLoader()。这是一个由 React Router 提供的特殊函数,它实际上会执行与该路由关联的 loader 函数的逻辑 。这意味着它可以复用 loader 的数据获取能力!
      • 数据合并 :它将 serverLoader() 返回的基础数据和从 API 获取的额外数据(或更新的数据)进行合并。例如,serverData 可能有 { name, description },而 API 返回的数据可能有 { liveStock: 50, reviewsCount: 188 }。最终返回给组件的是一个合并后的大对象。
  4. 组件更新 :React 使用 clientLoader 返回的数据重新渲染 <Product> 组件,更新页面内容。

  5. 用户体验:页面内容无刷新更新,应用感觉流畅、快速,就像一个桌面应用。

小结 :这种默认模式是最佳实践。loader 保证了最快的首屏速度,clientLoader 保证了后续导航的安全和高效,并通过 serverLoader() 实现了逻辑复用。


第二部分:增强模式(解释第二个代码块)

jsx 复制代码
export async function loader() { /* ... */ }
export async function clientLoader() { /* ... */ }

// 关键指令!
clientLoader.hydrate = true as const;

export function HydrateFallback() {
  return <div>Loading...</div>;
}

export default function Product() { /* ... */ }

这段代码展示了如何通过 hydrate = true 来改变默认行为,强制 clientLoader首次加载时也参与进来。

场景 C:用户首次访问页面 (但这次使用了 hydrate = true)

用户再次在浏览器中输入 http://your-site.com/products/123 并回车。

  1. 服务器 :和之前一样,服务器运行 loader 函数获取基础数据。

  2. 浏览器:浏览器接收到服务器发送的初始 HTML。

  3. 水合(Hydration)开始:浏览器下载完 JavaScript,React 开始接管页面。

  4. React Router 检查 :它发现这个路由的 clientLoader 上有一个 hydrate = true 的标记。

  5. 行为改变

    • React Router 暂停用服务器数据渲染最终组件。
    • 它会立即显示 <HydrateFallback> 组件。用户会看到 "Loading..."。
    • 同时,它在浏览器中执行 clientLoader 函数
  6. clientLoader 执行clientLoader 运行,它可能会:

    • localStorage 读取用户偏好(比如暗黑模式)。
    • 调用 serverLoader() 获取服务器的基础数据。
    • 将两者合并,形成一个最终的、完全个性化的数据对象
  7. 最终渲染clientLoader 返回数据后,React Router 使用这个新数据 来完成 <Product> 组件的渲染,替换掉 "Loading..."。

为什么需要这种增强模式?

这种模式的核心目的是为了在页面首次对用户可用之前,融合服务端数据和纯客户端数据

  • 没有 hydrate=true :页面先显示服务器给的内容,然后JS加载完,可能会再去读 localStorage,然后页面内容再变一次(比如从亮色模式"闪烁"到暗黑模式)。这种体验不佳。
  • 有了 hydrate=true:用户会先看到一个加载状态,然后直接看到根据其偏好设置好的最终界面,没有中间的"闪烁"过程。

总结

特性/场景 默认模式 (hydrate = false) 增强模式 (hydrate = true)
首屏加载时 只运行 loader (在服务器) 先运行 loader (服务器), 再运行 clientLoader (客户端)
客户端导航时 只运行 clientLoader (在客户端) 只运行 clientLoader (在客户端)
首屏用户体验 极快看到内容,但可能是通用版本 可能会先看到加载状态,但最终内容是完全个性化的
典型用途 大部分静态内容和通用数据展示 需要结合用户本地设置(如主题)、或依赖浏览器API才能获取数据的页面
数据流 SSR数据直接渲染 SSR数据 + 客户端数据 -> 合并后渲染

通过这种设计,React Router 提供了极大的灵活性,让开发者可以根据页面的具体需求,选择最优的数据加载策略。

相关推荐
小小小小宇1 小时前
TS泛型笔记
前端
小小小小宇1 小时前
前端canvas手动实现复杂动画示例
前端
codingandsleeping1 小时前
重读《你不知道的JavaScript》(上)- 作用域和闭包
前端·javascript
小小小小宇1 小时前
前端PerformanceObserver使用
前端
zhangxingchao2 小时前
Flutter中的页面跳转
前端
烛阴3 小时前
Puppeteer入门指南:掌控浏览器,开启自动化新时代
前端·javascript
全宝4 小时前
🖲️一行代码实现鼠标换肤
前端·css·html
小小小小宇4 小时前
前端模拟一个setTimeout
前端
萌萌哒草头将军4 小时前
🚀🚀🚀 不要只知道 Vite 了,可以看看 Farm ,Rust 编写的快速且一致的打包工具
前端·vue.js·react.js
芝士加5 小时前
Playwright vs MidScene:自动化工具“双雄”谁更适合你?
前端·javascript