Lesson 07:多页面架构 — React Router v7 基础
🧩 本节信息卡(学习前先看)
- 阶段定位:Phase 2(进阶篇)
- 推荐时长:75~120 分钟(首次学习)
- 先修要求:完成 Phase 1,理解组件拆分与基础状态管理
- 学习产出:完成本节功能,并能用自己的话解释关键设计取舍
✅ 本节完成标准(自检清单)
- [ ] 我可以独立复现文中的核心代码片段
- [ ] 我能解释“为什么这样实现”,而不只是“照着写”
- [ ] 我记录了至少 1 个踩坑点和修复方法
🧭 本节统一学习流程
- 学习目标:先明确本节要解决的业务问题与核心 API。
- 主线实战:跟随课程实现可运行功能(先跑通,再优化)。
- 原理深挖:理解为什么这样设计,以及常见误区。
- 练习挑战:完成 L1/L2(阶段收官课建议加 L3)巩固迁移能力。
- 本节小结:回顾“做了什么 / 学到了什么 / 下节前检查项”。
建议节奏:阅读 20% + 编码 60% + 复盘 20%。
🎯 本节目标:从单页面(SPA)升级为多页面应用,搭建任务管理系统的外壳。
📦 本节产出:带有四个独立页面(首页、项目列表、任务看板、设置)的基础路由骨架。
一、Phase 2 整体规划:任务管理系统
Phase 1 的 Todo App 只有一个页面,管理一个列表。 Phase 2 我们要开发一个 Trello 式的任务管理系统,包含:
- 多页面体验:React Router v7
- 复杂状态共享:Zustand
- 服务端模拟:TanStack Query(Mock API)
- 专业级 UI:shadcn/ui + Tailwind v4
1.1 项目结构预览
二、初始化 Phase 2 项目
重新创建一个空项目(或者在你现有的代码库旁边新建一个文件夹):
npm create vite@latest phase2-task-manager -- --template react-ts
cd phase2-task-manager
npm install
npm install tailwindcss @tailwindcss/vite配置 Tailwind v4(修改 vite.config.ts 和 src/index.css,同 Lesson 01)。
清理无用模板代码后,安装 React Router v7:
npm install react-routerTIP
React Router v7 的变化 v7 时代官方更推荐围绕 react-router 使用 Data Router 能力。本课示例统一从 react-router 导入。 如果你看到旧教程使用 react-router-dom,那通常是 v6 时代或传统组件路由写法。
三、路由基础:搭建 4 个基本页面
先在 src/pages 目录下创建 4 个极其简单的占位组件:
// src/pages/Home.tsx
export default function Home() {
return <div className="p-8"><h1 className="text-2xl font-bold">🏠 首页看板</h1></div>
}
// src/pages/Projects.tsx
export default function Projects() {
return <div className="p-8"><h1 className="text-2xl font-bold">📂 项目列表</h1></div>
}
// src/pages/ProjectBoard.tsx (注意:这是一个动态页面)
import { useParams } from 'react-router'
export default function ProjectBoard() {
const { id } = useParams() // 获取 URL 中的参数
return <div className="p-8"><h1 className="text-2xl font-bold">📋 看板 ID: {id}</h1></div>
}
// src/pages/Settings.tsx
export default function Settings() {
return <div className="p-8"><h1 className="text-2xl font-bold">⚙️ 设置页</h1></div>
}
// src/pages/NotFound.tsx
import { Link } from 'react-router'
export default function NotFound() {
return (
<div className="p-8 text-center">
<h1 className="text-4xl drop-shadow-sm text-red-500 font-bold mb-4">404 - 页面迷路了</h1>
<Link to="/" className="text-indigo-600 hover:underline">返回首页</Link>
</div>
)
}四、配置顶层路由 (Data Router 模式)
React Router v6.4+ 引入了基于对象的数据路由 (Data Router) 模式,v7 也是强推这种写法(而不是传统的 <Routes> 组件嵌套),因为它支持高级的数据加载 (Loader) 特性。
替换 src/main.tsx:
// src/main.tsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { createBrowserRouter, RouterProvider } from 'react-router'
import './index.css'
// 导入页面
import Home from './pages/Home'
import Projects from './pages/Projects'
import ProjectBoard from './pages/ProjectBoard'
import Settings from './pages/Settings'
import NotFound from './pages/NotFound'
// 1. 定义路由配置数组
const router = createBrowserRouter([
{
path: '/',
element: <Home />,
errorElement: <NotFound /> // 全局 404/错误 边界
},
{
path: '/projects',
element: <Projects />,
},
{
path: '/projects/:id', // 动态路由段 (:id)
element: <ProjectBoard />,
},
{
path: '/settings',
element: <Settings />,
}
])
// 2. 将 router 注入到应用根节点
createRoot(document.getElementById('root')!).render(
<StrictMode>
<RouterProvider router={router} />
</StrictMode>,
)此时启动项目:
- 访问
http://localhost:5173/→ 显示首页 - 访问
http://localhost:5173/projects→ 显示项目列表 - 访问
http://localhost:5173/projects/todo-app→ 页面会用useParams抓取并显示"看板 ID: todo-app" - 访问
http://localhost:5173/aaa→ 显示 404
五、用 Navbar 实现页面跳转
多页面应用不能直接用 <a href="/..."> 跳转,因为那会触发完整的浏览器刷新,丢失 React 的所有状态!我们需要用 <Link> 组件。
创建一个简单的临时导航条(下节课我们会升级为完整的 Layout + Sidebar):
// src/App.tsx (我们把它改造成主应用的壳)
import { Link, Outlet } from 'react-router'
export default function App() {
return (
<div className="min-h-screen bg-gray-50 flex flex-col">
{/* 顶部导航条 */}
<nav className="bg-indigo-600 text-white p-4 shadow-md flex gap-6">
<Link to="/" className="font-bold hover:text-indigo-200">🚀 TaskApp</Link>
<div className="flex gap-4 ml-8">
<Link to="/" className="hover:text-indigo-200">首页</Link>
<Link to="/projects" className="hover:text-indigo-200">项目</Link>
<Link to="/settings" className="hover:text-indigo-200">设置</Link>
</div>
</nav>
{/* 页面内容占位符 */}
<main className="flex-1">
<Outlet />
</main>
</div>
)
}等一下!App.tsx 写好了,但怎么让它生效呢?我们需要用到嵌套路由!
将 main.tsx 中的路由配置包裹起来:
// src/main.tsx (部分修改)
import App from './App'
const router = createBrowserRouter([
{
path: '/',
element: <App />, // 外层包裹组件
errorElement: <NotFound />,
children: [ // 内部嵌套页面
{ index: true, element: <Home /> }, // 默认子路由 (/)
{ path: 'projects', element: <Projects /> },
{ path: 'projects/:id', element: <ProjectBoard /> },
{ path: 'settings', element: <Settings /> }
]
}
])深入理解 <Outlet />
在这个结构下,无论你在哪个子页面切换,顶部的 Navbar 都永远不会被销毁和重新渲染(除非顶级 URL 改变),只有 <Outlet> 区域的内容在被替换,这就是 SPA 路由极其丝滑的原因。
六、🧠 深度专题:SPA 路由原理
为什么点击 <Link> 页面不会闪白刷新?
传统的 MPA(多页应用):
- 点击
<a href="/about"> - 浏览器卸载当前页面,向服务器请求
/about的 HTML - 服务器返回全新 HTML
- 浏览器重新解析、加载 CSS/JS,白屏闪烁
React SPA(单页应用)使用 Client-Side Routing (客户端路由):
这里面最核心的浏览器 API 是 History API:
history.pushState(): 改变 URL 而不刷新页面window.addEventListener('popstate', ...): 监听用户点击浏览器的"后退/前进"按钮
React Router 就是在这两者的基础上包了一层极其庞大和完善的 React 组件状态管理层。
七、练习
- 在
Projects.tsx页面中,手动写死三个项目的假数据,并使用<Link to="/projects/1">等链接指向它们。 - 体验
NavLink:导入import { NavLink } from 'react-router'替换 App.tsx 中的Link。NavLink的className可以接收一个函数({ isActive }) => ...,试着让当前激活的导航菜单变成高亮的颜色。
📌 本节小结
| 你做了什么 | 你学到了什么 |
|---|---|
| 搭建了任务管理系统的 4 个骨架页面 | v7 Data Router 的 createBrowserRouter |
| 使用嵌套路由包裹页面 | children 和 index: true 路由 |
创建了包含 Outlet 的全局 App | 父布局组件与 Outlet 插槽协作 |
| 实现了无刷新的 SPA 跳转 | <Link> 替代 <a href> |
| — | SPA 客户端路由原理 (History API) |
八、真实问题案例:刷新 404 与权限误判
8.1 为什么本地正常、上线刷新就 404?
SPA 的子路由(如 /projects/1)在浏览器刷新时会直接请求服务器该路径。若服务器未配置回退到 index.html,就会返回 404。
- Vercel / Netlify:通常内置回退能力
- Nginx:需要
try_files $uri $uri/ /index.html;
8.2 路由守卫常见误区
- 误区 1:只做前端守卫,不做服务端鉴权(可被绕过)
- 误区 2:把权限判断写死在组件内,导致多处重复
- 误区 3:未区分“未登录”和“无权限”,造成错误提示混乱
建议:路由层做体验守卫,服务端做最终授权。
8.3 验收标准(L1/L2)
- L1:实现 4 个页面互跳,并正确高亮当前菜单。
- L2:在本地模拟刷新子路由场景,给出 404 修复配置(Vercel/Netlify/Nginx 任一)。