本文基于 React 19 + Vite + React Router v7 的真实 Demo 项目,逐行拆解前端路由的核心用法------从最基础的路由配置,到懒加载、动态参数、嵌套路由、编程式导航、重定向与 404 兜底,帮你建立起对现代前端路由的完整认知。
一、项目背景与技术栈
这是一个基于 Vite 脚手架创建的 React 项目,核心依赖:
json
{
"dependencies": {
"react": "^19.2.7",
"react-dom": "^19.2.7",
"react-router-dom": "^7.18.2"
}
}
React 19 + React Router v7,是目前最新的一套前端路由技术栈。
二、入口:一切从 createRoot 开始
jsx
// src/main.jsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>,
)
React 19 的入口写法和 React 18 一致:用 createRoot 挂载到 #root 节点,外层包一个 StrictMode(开发环境下帮你发现潜在问题的辅助工具)。真正的内容全在 <App /> 里。
三、路由核心配置:App.jsx 全景
这是整个应用的核心,信息量很大,我们逐块拆解。
jsx
// src/App.jsx
import { lazy, Suspense } from 'react';
import {
HashRouter as Router,
Routes,
Route,
Navigate
} from 'react-router-dom';
import Navigation from './components/Navigation';
// 路由懒加载
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const UserProfile = lazy(() => import('./pages/UserProfile'));
const NotFound = lazy(() => import('./pages/NotFound'));
const Products = lazy(() => import('./pages/Products'));
const ProductDetail = lazy(() => import('./pages/Products/Detail'));
const NewProduct = lazy(() => import('./pages/Products/New'));
const App = () => {
return (
<>
<Suspense fallback={<div>Loading...</div>}>
<Router>
<Navigation />
<div id="container">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/user/:id" element={<UserProfile />} />
<Route path="*" element={<NotFound />} />
<Route path="/products" element={<Products />}>
<Route path=":productId" element={<ProductDetail />} />
<Route path="new" element={<NewProduct />} />
</Route>
<Route path="/old-path" element={
<Navigate replace to="/new-path" />
} />
</Routes>
</div>
</Router>
</Suspense>
</>
)
}
export default App
3.1 HashRouter:路由的"壳"
jsx
import { HashRouter as Router } from 'react-router-dom';
<Router>
{/* 所有路由相关的内容都放在这里面 */}
</Router>
HashRouter 是路由的根容器,它用 #/ 形式的 hash 来管理 URL(如 localhost:5173/#/about)。
为什么用 HashRouter 而不是 BrowserRouter?因为 hash 路由不需要服务器配置,纯前端就能跑,适合开发和部署简单的场景。如果你用 BrowserRouter(History 模式),URL 会更干净(localhost:5173/about),但服务器需要配置回退规则,否则刷新会 404。
底层原理就是上一篇文章讲的 hashchange 事件------React Router 只是用组件化的方式帮你封装了。
3.2 路由懒加载:用 lazy + Suspense 拆包
jsx
import { lazy, Suspense } from 'react';
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
// ...
<Suspense fallback={<div>Loading...</div>}>
<Router>
{/* ... */}
</Router>
</Suspense>
这是 SPA 性能优化的关键手段。
如果不做懒加载,所有页面的代码会被打包进一个巨大的 JS 文件,用户首次打开首页时要把 About、Products、UserProfile......所有页面的代码都下载下来,严重拖慢首屏速度。
lazy() 让每个页面变成一个独立的代码块(chunk),只有用户真正访问到该路由时才会按需加载。Suspense 的 fallback 则是加载过程中显示的占位 UI。
用户访问首页 → 只下载 Home.js
用户点击 About → 按需下载 About.js(显示 Loading...)
用户点击 Products → 按需下载 Products.js
这就是 SPA 的精髓:下载、执行,只针对当前页面。
3.3 Routes 与 Route:声明式路由配置
jsx
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/user/:id" element={<UserProfile />} />
<Route path="*" element={<NotFound />} />
{/* ... */}
</Routes>
Routes 是路由配置的容器,内部是一组 Route。React Router 的设计理念是一切皆组件------路由配置本身就是 JSX,既是"声明在哪里",又是"渲染在哪里"。
关键规则:Routes 内部同一时刻只有一个 Route 匹配并渲染。当前 URL 对应哪个 path,就显示哪个组件。
3.4 动态路由参数::id
jsx
<Route path="/user/:id" element={<UserProfile />} />
:id 是一个动态参数占位符。当用户访问 /#/user/666 时,666 会被捕获为 id 参数。
在组件内通过 useParams 获取:
jsx
// src/pages/UserProfile/index.jsx
import { useParams } from 'react-router-dom'
function UserProfile() {
let { id } = useParams();
console.log(id); // "666"
return (
<>
<h1>user Profile: {id}</h1>
</>
)
}
useParams 是一个 Hook,返回一个包含所有路径参数的对象。这里体现了 React 的 Hooks 思想------把路由状态"钩"进函数组件,不需要通过 props 层层传递。
3.5 嵌套路由与 Outlet:多级路由
这是 React Router 最强大的特性之一。
jsx
<Route path="/products" element={<Products />}>
<Route path=":productId" element={<ProductDetail />} />
<Route path="new" element={<NewProduct />} />
</Route>
父路由 /products 渲染 <Products /> 组件,子路由在父组件内部通过 <Outlet /> 标记渲染出口:
jsx
// src/pages/Products/index.jsx
import { Outlet } from 'react-router-dom';
function Products() {
return (
<>
<h1>Products</h1>
<Outlet /> {/* 子路由内容渲染在这里 */}
</>
)
}
URL 对应关系:
| URL | 渲染结果 |
|---|---|
/#/products |
Products(Outlet 为空) |
/#/products/123 |
Products + ProductDetail(productId=123) |
/#/products/new |
Products + NewProduct |
嵌套路由的精髓在于:父组件的布局(导航、侧边栏等)保持不变,只有 Outlet 区域切换内容。这比手动管理页面切换优雅得多。
3.6 编程式导航:useNavigate
jsx
// src/pages/NotFound/index.jsx
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
const NotFound = () => {
let navigate = useNavigate();
useEffect(() => {
setTimeout(() => {
navigate('/'); // 3秒后跳回首页
}, 3000)
}, [])
return(
<>
<h1>404 Not Found</h1>
</>
)
}
useNavigate 返回一个 navigate 函数,可以在代码中触发路由跳转。常见场景:
- 表单提交后跳转
- 权限校验失败后重定向
- 倒计时自动跳转(如本例的 404 → 首页)
注意这里没有用 window.location.href = '/'(会整页刷新),而是用 navigate('/')(纯前端路由跳转,不刷新)。这就是 SPA 跳转和传统跳转的本质区别。
3.7 重定向:Navigate 组件
jsx
<Route path="/old-path" element={
<Navigate replace to="/new-path" />
} />
<Navigate> 是一个声明式重定向组件。当用户访问 /old-path 时,会自动跳转到 /new-path。replace 属性表示用 history.replaceState 而非 pushState------替换当前历史记录而不是新增一条,用户点"后退"不会回到 /old-path。
适用场景:旧 URL 迁移、权限守卫中的未登录跳转。
3.8 404 兜底:通配符路由
jsx
<Route path="*" element={<NotFound />} />
* 是贪婪匹配,会匹配所有未命中的路径。把它放在 Routes 的最后,就成了 404 兜底。无论用户输入什么乱七八糟的 URL,都不会白屏,而是显示 404 页面并自动跳回首页。
四、导航组件:用 Link 代替 a 标签
jsx
// src/components/Navigation.jsx
import { Link } from 'react-router-dom';
function Navigation() {
return (
<nav>
<ul>
<li><Link to="/">Home</Link></li>
<li><Link to="/about">About</Link></li>
<li><Link to="/user/666">小家</Link></li>
<li><Link to="/products">Products</Link></li>
<li><Link to="/products/123">ProductDetail</Link></li>
<li><Link to="/products/new">商品新增</Link></li>
</ul>
</nav>
);
}
为什么不直接用 <a href="#/about">?
因为原生 <a> 标签点击后会触发浏览器的默认导航行为。React Router 的 <Link> 组件在内部拦截了点击事件,改用 pushState / hash 变化来实现 SPA 式的无刷新跳转。用 to 属性指定目标路径,语义清晰,也方便框架做预加载等优化。
五、路由能力全景图
把上面所有用法汇总,一个完整的 React Router 应用包含这些核心能力:
| 能力 | API | 用途 |
|---|---|---|
| 路由容器 | <HashRouter> / <BrowserRouter> |
管理全局路由状态 |
| 路由配置 | <Routes> + <Route> |
声明 URL → 组件映射 |
| 懒加载 | lazy() + <Suspense> |
按需加载,优化首屏 |
| 动态参数 | :id + useParams() |
URL 中提取变量 |
| 嵌套路由 | 子 <Route> + <Outlet /> |
多级路由,布局复用 |
| 声明式跳转 | <Link to="..."> |
无刷新导航 |
| 编程式跳转 | useNavigate() |
代码中触发路由跳转 |
| 重定向 | <Navigate replace to="..."> |
URL 迁移、权限守卫 |
| 404 兜底 | path="*" |
未匹配路由的兜底页面 |
六、从原理到框架:一句话串起两篇文章
如果你读过上一篇《从多页到SPA:前端 Hash 路由原理》,你会发现 React Router 做的事情和手写的 HashRouter 本质完全一样:
xml
原生 hashchange 事件
↓ 封装成组件
<HashRouter> 监听 hash 变化
↓ 声明式配置
<Routes><Route> 路由表
↓ 按需加载
lazy + Suspense 代码分割
↓ 渲染出口
#container → <Outlet />
框架没有魔法,只是把朴素的原生 API 用组件化的方式重新组织了一遍。理解了 hashchange 这个底层机制,React Router 的所有 API 都只是它的不同表达形式。
学原理是为了不被框架吓住,学框架是为了不再重复造轮子。 两者结合,才是完整的前端能力。