讲解
React 本身没有路由,社区的事实标准是 React Router(v7 已融合 Remix 的能力,但核心的声明式路由用法没变)。核心概念和 Vue Router 对仗:路由表(path → 组件)、<Outlet>(子路由渲染出口)、<Link>(声明式导航,代替 a 标签避免整页刷新)。
现代写法用 createBrowserRouter 配路由对象数组:{ path: '/', element: <Layout/>, children: [...] } 嵌套路由让布局组件只写一次,<Outlet> 处渲染匹配的子路由。动态参数 path: 'user/:id',组件里 useParams() 取参;查询字符串用 useSearchParams;编程式导航 useNavigate:navigate('/login')、navigate(-1) 后退。404 兜底用 path: '*'。
React Router 的数据 API 是它的差异化能力:路由可以配 loader(渲染前加载数据)和 action(处理表单提交),组件里 useLoaderData() 拿数据——数据在导航开始时就开始拉取,而不是组件挂载后才开始(消除「渲染→请求」瀑布)。守卫模式随之改变:登录校验写在 loader 里 redirect('/login'),而不是组件里 useEffect 检查。
懒加载用 React.lazy + Suspense 包路由组件,或者 v7 的 lazy 路由属性。路径相对性有个小坑:嵌套路由里的 <Link to="settings"> 是相对当前路由的,想绝对跳转写 to="/settings"。
示例
实现迷你路由匹配 + loader 数据预加载,看「导航即加载」如何避免瀑布(在本教程构建时被真实执行):
import assert from 'node:assert/strict';
// 路径匹配(同 Vue 章节的思路,这里支持嵌套树)
function match(routes, path) {
for (const r of routes) {
if (r.path === '*') continue; // 通配兜底由路由器单独处理,这里只匹配具体路径
const keys = [];
const pattern = r.path
.split('/')
.map((s) => {
if (s.startsWith(':')) {
keys.push(s.slice(1));
return '([^/]+)';
}
return s;
})
.join('/');
const m = path.match(new RegExp('^' + pattern + '$'));
if (m) return { route: r, params: Object.fromEntries(keys.map((k, i) => [k, m[i + 1]])) };
}
return null;
}
const routes = [
{ path: '/', page: 'Home' },
{ path: '/user/:id', page: 'User' },
{ path: '*', page: 'NotFound' },
];
assert.strictEqual(match(routes, '/user/42').params.id, '42');
assert.strictEqual(match(routes, '/nope'), null); // 无匹配时交给 path: '*' 兜底路由
// loader 模式:匹配时就开始加载数据,渲染时数据已就绪
async function navigate(path, loaders) {
const started = Date.now();
const data = await loaders[path](); // 导航即加载
return { page: path, data, elapsed: Date.now() - started };
}
const wait = (ms, v) => () => new Promise((r) => setTimeout(() => r(v), ms));
const result = await navigate('/user/42', { '/user/42': wait(60, { name: '小明' }) });
assert.strictEqual(result.data.name, '小明');
assert.ok(result.elapsed >= 55);
// 对比瀑布模式:先渲染(30ms)再请求(60ms) = 90ms;loader 模式只有 60ms
const waterfall = 30 + 60;
const loaderTime = 60;
assert.ok(loaderTime < waterfall);
console.log('匹配 /user/42 →', JSON.stringify(match(routes, '/user/42')));
console.log('loader 模式', loaderTime, 'ms vs 瀑布模式', waterfall, 'ms——数据与导航并行');
真实的 React Router 配置:
import { createBrowserRouter, Link, Outlet, useLoaderData, useParams } from 'react-router';
const router = createBrowserRouter([
{
path: '/',
element: <Layout />,
children: [
{ index: true, element: <Home /> },
{
path: 'user/:id',
loader: async ({ params }) => {
const res = await fetch('/api/users/' + params.id); // 导航即加载
if (!res.ok) throw new Response('Not Found', { status: 404 });
return res.json();
},
element: <User />,
},
{ path: '*', element: <NotFound /> },
],
},
]);
function Layout() {
return (
<>
<nav>
<Link to="/">首页</Link>
</nav>
<Outlet />
</>
);
}
function User() {
const user = useLoaderData(); // loader 的数据,无瀑布
const { id } = useParams();
return <h1>{user.name}(#{id})</h1>;
}
常见坑
- 用 a 标签站内跳转:整页刷新状态全丢;用 Link 或 useNavigate。
- 相对路径踩坑:嵌套路由里 to="settings" 是相对路径,确认要相对还是绝对(/ 开头)。
- 组件里 useEffect 检查登录:跳转闪烁(先渲染再跳走);守卫写 loader 里 redirect。
- loader 里读 localStorage 做鉴权:SSR 时没有 localStorage,鉴权信息走 cookie/请求。
- v6 老写法 Routes/Routes 与新数据路由混用:createBrowserRouter 体系才有 loader/action,选一个体系用到底。
小结
createBrowserRouter + 嵌套路由 + Outlet/Link;loader 让数据与导航并行消除瀑布;动态参数 useParams,编程导航 useNavigate。下一章讲全局状态管理的选型。