React Router v7 实战:用路由配置、懒加载与嵌套路由搭建一个完整的 SPA

本文基于 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),只有用户真正访问到该路由时才会按需加载。Suspensefallback 则是加载过程中显示的占位 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-pathreplace 属性表示用 history.replaceState 而非 pushState------替换当前历史记录而不是新增一条,用户点"后退"不会回到 /old-path

适用场景:旧 URL 迁移、权限守卫中的未登录跳转。

3.8 404 兜底:通配符路由

jsx 复制代码
<Route path="*" element={<NotFound />} />

* 是贪婪匹配,会匹配所有未命中的路径。把它放在 Routes 的最后,就成了 404 兜底。无论用户输入什么乱七八糟的 URL,都不会白屏,而是显示 404 页面并自动跳回首页。

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 都只是它的不同表达形式。

学原理是为了不被框架吓住,学框架是为了不再重复造轮子。 两者结合,才是完整的前端能力。

相关推荐
引山洪083 小时前
Babylon.js 8.x 中文文档整理——Node篇 (上)
前端·webgl
柒和远方3 小时前
V058:前端路由的第一性原理:从 hashchange 手写路由,到 React Router 的嵌套与懒加载
前端·javascript·react.js
计算机魔术师3 小时前
阿里云上线 One Key MCP 服务:兼容 Qoder、Codex 等,可一键调用多家 MCP 服务
前端
Darling噜啦啦3 小时前
React Router 全家桶实战:从路由懒加载到嵌套路由的 6 大核心玩法
前端·react.js
Goodbye3 小时前
组件详解:从起源到未来的全方位解读
前端
无糖可可果3 小时前
从前端路由的起源到 React Router 实战
前端
亿元程序员3 小时前
竹知了很火?于是我用Cocos做了一个
前端
八号当铺3 小时前
我做了一个多端基金收益助手:从养基宝数据到 Web、桌面端、浏览器插件和 IDE 插件
前端·人工智能·github
用户852495071843 小时前
从多页到 SPA:用 50 行代码理解前端路由的本质
前端