首页
/ React Router 渐进式增强(Progressive Enhancement)实战指南:基于 HTML 构建快速、稳健且简单的框架模式应用

React Router 渐进式增强(Progressive Enhancement)实战指南:基于 HTML 构建快速、稳健且简单的框架模式应用

2026-09-07 19:34:46作者:卓炯娓

渐进式增强(Progressive Enhancement)是一种"内容优先"的 Web 设计策略:先让所有用户都能访问页面最基本的内容与功能,再为具备更强浏览器能力或更快网络的用户叠加增强体验。本文以 React Router(framework 模式,默认启用服务端渲染 SSR)为背景,系统讲解渐进式增强的三大价值(性能、弹性、简单性),并基于 LinkFormuseFetcheruseNavigation 等核心 API 演示如何"先交付最简可用的 HTML 版本,再以增量迭代方式叠加客户端增强",同时深入源码剖析其底层实现原理。

适用范围:本文对应文档标注为 [MODES: framework],即面向 React Router 框架模式(framework mode)下默认启用 Server-Side Rendering(SSR)的场景。React Router 的声明式(declarative)模式与数据模式(data mode)同样提供 Link/Form 等组件,但渐进式增强带来的收益在框架模式的 SSR 默认路径下体现得最为直接。

渐进式增强为什么重要

Progressive enhancement is a strategy in web design that puts emphasis on web content first, allowing everyone to access the basic content and functionality of a web page, whilst users with additional browser features or faster Internet access receive the enhanced version instead.

—— Wikipedia

"渐进式增强"一词由 Steven Champeon 与 Nick Finck 于 2003 年提出,诞生于各浏览器对 CSS/JavaScript 支持参差不齐、许多用户甚至完全禁用 JavaScript 的时代。如今我们面对的浏览器环境已经高度一致,绝大多数用户也开启了 JavaScript,但 React Router 仍然坚持渐进式增强的核心原则,因为它能带来更快的应用更稳健的应用更简单的开发工作流

  • 性能(Performance):虽然你可能觉得只有 5% 的用户网速很慢,但现实是——100% 的用户在 5% 的时间里网速都很慢
  • 弹性(Resilience):在 JavaScript 加载完成之前,每个人都处于"禁用 JavaScript"的状态。以 HTML 为地基的应用天然能扛住这段空白期。
  • 简单性(Simplicity):用渐进式增强的方式使用 React Router 构建应用,实际上比构建传统 SPA 更简单——因为大量原本需要客户端状态管理处理的逻辑,可以交给浏览器与 URL 原生能力。

性能:框架模式让首屏与导航更快

服务端渲染允许 React Router 应用比典型 单页应用 SPA 并行完成更多工作,从而加快首次加载体验与后续导航。

典型 SPA 首先下发一个近乎空白的 HTML 文档,页面上的所有工作都要等 JavaScript 加载完成之后才开始:

HTML        |---|
JavaScript      |---------|
Data                      |---------------|
                            page rendered 👆

React Router 应用则可以在请求到达服务器的瞬间就开始工作,并把响应以流(stream)的形式发给浏览器,让浏览器能够并行下载 JavaScript、其他静态资源与数据:

               👇 first byte
HTML        |---|-----------|
JavaScript      |---------|
Data        |---------------|
              page rendered 👆

注意对比两条时间线:SPA 的渲染完成点(page rendered 👆)被数据加载拖到最后;而在 React Router 中,HTML 的第一个字节下发后,JavaScript 与数据几乎同时并行开始拉取,页面渲染完成点大幅前移。

补充背景:若想构建纯 SPA(无运行时 SSR),React Router 框架模式也支持通过 ssr: false 启用 SPA 模式——但那是另一条技术路线。渐进式增强的核心阵地是 SSR 默认开启的框架模式,这也是为什么对应文档将适用范围标注为 framework 模式。相关对比可参考 Single Page App (SPA) 指南

弹性与可访问性:先让一切在"无 JS"下工作,再叠加增强

用户大概率不会主动关闭 JavaScript 浏览网页,但在 JS 尚未加载完的每一刻,所有人其实都在"无 JS"地使用网站。React Router 拥抱渐进式增强的方式是:构建在 HTML 之上——先用纯 HTML 能力把功能跑通,再以 JavaScript 为"增强层"逐级叠加体验。

最简单的案例:<Link> 渲染成真实的 <a href>

<Link to="/account">Account</Link>

这个组件在服务端/无 JS 环境下渲染为原生 <a href="/account"> 标签,点击即可跳转,无需任何 JavaScript。当 JavaScript 加载完成后,React Router 会拦截这次点击,改用客户端路由(client side routing)完成导航,从而让开发者掌控更丰富的用户体验——而不只是在浏览器标签页上转圈圈。无论 JS 是否可用,链接都能正常工作。

packages/react-router/lib/dom/lib.tsxLink 组件的实现可以看到这一机制的完整闭环(源码约 第 1315 行起):

  • 无论 JS 是否加载,组件都会渲染出带真实 href<a> 元素(hrefuseHref 解析生成,源码 第 1343 行);
  • 组件通过 useLinkClickHandler 生成内部点击处理函数(第 1377 行),并在 JS 可用时注册 onClick 拦截事件、走客户端导航;
  • 用户显式传入 onClick 时先执行用户的回调,只有未被 preventDefault 时才会触发内部客户端导航逻辑(handleClick,源码 第 1388-1395 行);
  • 当目标是外部链接或显式设置了 reloadDocument(强制整页刷新导航)时,则不挂客户端拦截。

这正是"底层是可访问的 HTML,上层是增强的客户端路由"这一设计思想的直接体现。

提交表单:一个不依赖 JavaScript 的"加入购物车"按钮

再看一个简单的"加入购物车"按钮:

import { Form } from "react-router";

export function AddToCart({ id }: { id: string }) {
  return (
    <Form method="post" action="/add-to-cart">
      <input type="hidden" name="id" value={id} />
      <button type="submit">Add To Cart</button>
    </Form>
  );
}

无论 JavaScript 是否加载,这个按钮都能把商品加入购物车。区别只在于由谁来执行提交

  • 无 JS 时:这是浏览器原生 <form method="post">,由浏览器自己负责序列化表单数据、发起 POST 请求并接管 pending 状态(如标签页转圈);
  • 有 JS 时:React Router 拦截这次表单提交,改为在客户端处理——action 在客户端触发、数据提交走 fetch、页面数据自动 revalidate,你可以叠加自己的 pending UI 与其他客户端行为。

源码层面:Form 为什么能"双态可用"

Form 的实现细节(Form 定义,lib/dom/lib.tsx 第 1922 行)可以拆解为三点:

  1. 渲染成真正的 HTML <form>:组件最终输出原生 <form method=... action=... onSubmit=...>第 1982-1993 行),methodaction 都是真实存在的 HTML 属性,天然可被无 JS 的浏览器处理。
  2. 仅当存在 JS 环境才挂上拦截器:组件始终绑定 submitHandler,但只有在需要客户端处理时才 event.preventDefault() 阻止浏览器默认提交(第 1950-1980 行);而显式设置 reloadDocument 时则完全退回原生行为,直接由浏览器整页提交。
  3. method 被限定为原生能力范围:源码中 formMethod 的计算逻辑是 method.toLowerCase() === "get" ? "get" : "post"第 1945-1946 行)。这一点非常关键——原生 <form> 只支持 GETPOST 两种动词,因此 Form 组件 props 的类型注释也明确提醒(SharedFormProps,第 1739 行起):组件支持 deletepatchput 等扩展动词,但若想保留渐进式增强能力,应只用 getpost,否则无 JS 环境将无法工作。

此外,encType 默认值为 application/x-www-form-urlencoded,也支持 multipart/form-datatext/plain,全部与 HTML 原生枚举对齐,保证浏览器能在无 JS 时正确编码提交。

简单性:以迭代代替"双轨开发"

当你开始依赖 HTML、URL 这类 Web 基础能力时,会发现对客户端 state 与状态管理库的依赖显著减少。渐进式增强的正确心法是:"以迭代的方式构建,而不是为 JS 和无 JS 各写一套"——先交付功能最简单的版本并上线,再迭代到增强的用户体验。

示例一:购物车按钮 → 加入客户端行为

沿用上面的购物车按钮,在不改动功能根本设计的前提下,叠加一层客户端行为:

import { useFetcher } from "react-router";

export function AddToCart({ id }: { id: string }) {
  const fetcher = useFetcher();

  return (
    <fetcher.Form method="post" action="/add-to-cart">
      <input name="id" value={id} />
      <button type="submit">
        {fetcher.state === "submitting" ? "Adding..." : "Add To Cart"}
      </button>
    </fetcher.Form>
  );
}

这个版本在 JavaScript 加载期间的工作方式与之前完全相同(浏览器原生提交),而一旦 JavaScript 就绪:

  • useFetcher 不再像 <Form> 那样引发页面导航,用户停留在当前页面继续购物,不会被打断;
  • pending UI 由应用代码决定(按钮文字变成 "Adding..."),而不是浏览器标签页转圈。

源码层面:useFetcher 如何实现"导航但不动 URL"

useFetcher 实现(lib/dom/lib.tsx 第 2910 行)揭示了关键机制:

  • fetcher.Form 本质上是把 Form 组件以 navigate: false 的方式复用的产物(第 2969-2979 行)——同一套"渐进增强 HTML 表单"的封装,仅仅在内部通过 navigate: false 关掉了导航语义;
  • 组件通过 React.useId() 生成独立的 fetcherKey 注册到路由器(第 2929-2941 行),随后从路由状态中读取该 fetcher 的状态(statedataformData),供组件渲染 pending UI(第 2982-2994 行)。

所以用户的体验是渐进增强的,而开发者的 UI 也同样是"渐进增强的"——在不改变功能基本设计的前提下,逐步加入更丰富的交互。

示例二:以 URL 作为状态来源的搜索框

另一个能体现"渐进式增强带来简单性"的例子是 URL。当从 URL 出发构建 UI 时,你根本不需要操心客户端状态管理——URL 本身就是 UI 的单一事实来源(source of truth):

export function SearchBox() {
  return (
    <Form method="get" action="/search">
      <input type="search" name="query" />
      <SearchIcon />
    </Form>
  );
}

这个组件不需要任何状态管理:它只是一个提交到 /search 的普通 HTML 表单,GET 方法会把 query 放进 URL。JavaScript 加载后,React Router 拦截提交并在客户端完成导航。下面是可以直接迭代的下一个增强版本:

import { useNavigation } from "react-router";

export function SearchBox() {
  const navigation = useNavigation();
  const isSearching = navigation.location.pathname === "/search";

  return (
    <Form method="get" action="/search">
      <input type="search" name="query" />
      {isSearching ? <Spinner /> : <SearchIcon />}
    </Form>
  );
}

两版代码在架构上没有任何根本性变化——搜索状态保存在 URL(search params)中,useNavigation 只是读取了 React Router 内部正在进行的导航信息,据此把图标换成 spinner。对用户是渐进增强,对代码同样是渐进增强。

提示:useNavigationhooks.tsx 第 1494 行)在没有任何导航进行时默认返回 "idle" 的导航对象,因此 navigation.statenavigation.locationnavigation.formData 等字段可以安全读取。当 <Form method="get"> 引发的 GET 导航进行中时,navigation.location 会指向目标 /search URL,从而让组件感知"正在搜索"。

从搜索框延伸到状态管理的整体思路

"尽量用 URL 承载 UI 状态"与 React Router 对状态管理的整体主张一脉相承:与其把网络状态、pending 提交、服务器数据统统塞进 React state,不如直接利用 React Router 已经管理好的 useNavigationuseFetcherloaderDataactionData,或把状态放回 URL search params、Cookies、Server Sessions 等更自然的归宿。这部分完整论述参见 State Management(状态管理)说明文档

核心模式小结:一条可复用的迭代路径

综合以上案例,React Router 框架模式下的渐进式增强可以沉淀为一条固定的迭代路径:

步骤 你要做的事 无 JS 时的行为 JS 加载后的增强行为
1. 从 HTML 起步 <Link> 渲染链接、用 `<Form method="post" "get">` 渲染表单,URL 承载状态 浏览器原生导航/提交,一切可用
2. 加入 fetcher 把会打断用户流程的 <Form> 换成 useFetcher<fetcher.Form> 仍是原生表单提交,行为不变 停留在当前页面,不引发导航
3. 感知 pending 状态 读取 fetcher.state / navigation.location 渲染自己的 pending UI 无 UI 变化或仅基础 UI 获得定制的加载/提交中体验

实践要点:

  • 导航类交互优先用 <Link><Form>(它们会给 History 栈添加记录);不希望改变 URL 的提交优先用 <fetcher.Form>
  • 想要保留渐进增强,<Form> / <fetcher.Form>method 只应使用 get / post(见 SharedFormProps 类型注释)。
  • 从"最简版本"上线,再逐步"增强",而不是同时维护两套实现——这是渐进式增强降低复杂度的根本原因。

延伸阅读

围绕渐进式增强与其他相关主题,仓库文档中还提供了以下配套资料可供继续深入:

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
595
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
918
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.6 K
1.02 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
517
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
389