React Router 渐进式增强(Progressive Enhancement)实战指南:基于 HTML 构建快速、稳健且简单的框架模式应用
渐进式增强(Progressive Enhancement)是一种"内容优先"的 Web 设计策略:先让所有用户都能访问页面最基本的内容与功能,再为具备更强浏览器能力或更快网络的用户叠加增强体验。本文以 React Router(framework 模式,默认启用服务端渲染 SSR)为背景,系统讲解渐进式增强的三大价值(性能、弹性、简单性),并基于 Link、Form、useFetcher、useNavigation 等核心 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.tsx 中 Link 组件的实现可以看到这一机制的完整闭环(源码约 第 1315 行起):
- 无论 JS 是否加载,组件都会渲染出带真实
href的<a>元素(href由useHref解析生成,源码 第 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 行)可以拆解为三点:
- 渲染成真正的 HTML
<form>:组件最终输出原生<form method=... action=... onSubmit=...>(第 1982-1993 行),method与action都是真实存在的 HTML 属性,天然可被无 JS 的浏览器处理。 - 仅当存在 JS 环境才挂上拦截器:组件始终绑定
submitHandler,但只有在需要客户端处理时才event.preventDefault()阻止浏览器默认提交(第 1950-1980 行);而显式设置reloadDocument时则完全退回原生行为,直接由浏览器整页提交。 - method 被限定为原生能力范围:源码中
formMethod的计算逻辑是method.toLowerCase() === "get" ? "get" : "post"(第 1945-1946 行)。这一点非常关键——原生<form>只支持GET与POST两种动词,因此Form组件 props 的类型注释也明确提醒(SharedFormProps,第 1739 行起):组件支持delete、patch、put等扩展动词,但若想保留渐进式增强能力,应只用get和post,否则无 JS 环境将无法工作。
此外,encType 默认值为 application/x-www-form-urlencoded,也支持 multipart/form-data 与 text/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 的状态(state、data、formData),供组件渲染 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。对用户是渐进增强,对代码同样是渐进增强。
提示:
useNavigation(hooks.tsx 第 1494 行)在没有任何导航进行时默认返回 "idle" 的导航对象,因此navigation.state、navigation.location、navigation.formData等字段可以安全读取。当<Form method="get">引发的 GET 导航进行中时,navigation.location会指向目标/searchURL,从而让组件感知"正在搜索"。
从搜索框延伸到状态管理的整体思路
"尽量用 URL 承载 UI 状态"与 React Router 对状态管理的整体主张一脉相承:与其把网络状态、pending 提交、服务器数据统统塞进 React state,不如直接利用 React Router 已经管理好的 useNavigation、useFetcher、loaderData、actionData,或把状态放回 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 类型注释)。 - 从"最简版本"上线,再逐步"增强",而不是同时维护两套实现——这是渐进式增强降低复杂度的根本原因。
延伸阅读
围绕渐进式增强与其他相关主题,仓库文档中还提供了以下配套资料可供继续深入:
- Single Page App (SPA) 指南:了解
ssr: false的 SPA 模式与传统 SPA 形态的差异; - State Management(状态管理)说明:深入探讨为何以 URL 与服务器数据为状态来源可以减少客户端状态管理;
- 想直接上手体验 framework 模式的完整工程骨架,可参考 framework 模式指南、数据加载与提交、actions 与 pending UI。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00