Skip to content

Lesson 07:多页面架构 — React Router v7 基础 ​

🧩 本节信息卡(学习前先看) ​

  • 阶段定位:Phase 2(进阶篇)
  • 推荐时长:75~120 分钟(首次学习)
  • 先修要求:完成 Phase 1,理解组件拆分与基础状态管理
  • 学习产出:创建四个路由页面和404页,并完成内部导航
✅ 本节完成标准(自检清单)
  • [ ] 我可以独立复现文中的核心代码片段
  • [ ] 我能解释“为什么这样实现”,而不只是“照着写”
  • [ ] 我记录了至少 1 个踩坑点和修复方法

🧭 本节统一学习流程 ​

  1. 学习目标:先明确本节要解决的业务问题与核心 API。
  2. 主线实战:跟随课程实现可运行功能(先跑通,再优化)。
  3. 原理深挖:理解为什么这样设计,以及常见误区。
  4. 练习挑战:完成 L1/L2(阶段收官课建议加 L3)巩固迁移能力。
  5. 本节小结:回顾“做了什么 / 学到了什么 / 下节前检查项”。

建议节奏:阅读 20% + 编码 60% + 复盘 20%。

🎯 本节目标:在 SPA 中增加多个路由页面,搭建任务管理系统的外壳。

📦 本节产出:带有四个独立页面(首页、项目列表、任务看板、设置)的基础路由骨架。

一、Phase 2 整体规划:任务管理系统 ​

Phase 1 的 Todo App 只有一个页面,管理一个列表。 Phase 2 我们要开发一个 Trello 式的任务管理系统,包含:

  • 多页面体验:React Router v7
  • 复杂状态共享:Zustand
  • 服务端模拟:json-server;TanStack Query 管理请求与缓存
  • 专业级 UI:shadcn/ui + Tailwind v4

1.1 项目结构预览 ​


二、初始化 Phase 2 项目 ​

在 Phase 1 项目旁创建独立项目。沿用 Lesson 01 的 Node 24 LTS 环境;本阶段固定 React 19、Vite 7 与 React Router 7,避免 latest 跨大版本改变模板:

bash
npm create vite@8 phase2-task-manager -- --template react-ts
cd phase2-task-manager
npm install
npm install tailwindcss@4 @tailwindcss/vite@4

配置 Tailwind v4(修改 vite.config.ts 和 src/index.css,同 Lesson 01)。

清理无用模板代码后,安装 React Router v7:

bash
npm install react-router@7

TIP

React Router v7 的变化 本课选择 Data Mode,使用 react-router@7。浏览器端的 RouterProvider 从 react-router/dom 导入,其余路由 API 从 react-router 导入。v7 也支持 Declarative Mode 与 Framework Mode,应按需要选择;react-router-dom 在 v7 仍是兼容入口。参见 RouterProvider 官方说明。


三、路由基础:搭建 4 个基本页面 ​

先在 src/pages 目录下分别创建 4 个页面文件,以及用于未匹配路径的 NotFound.tsx。下面每段文件注释都代表一个独立文件:

tsx
// 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) API。本课使用 createBrowserRouter,为后续的 loader、action 和路由错误处理预留入口。

替换 src/main.tsx:

tsx
// src/main.tsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { createBrowserRouter } from 'react-router'
import { RouterProvider } from 'react-router/dom'
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 />,
  },
  {
    path: '/projects',
    element: <Projects />,
  },
  {
    path: '/projects/:id',  // 动态路由段 (:id)
    element: <ProjectBoard />,
  },
  {
    path: '/settings',
    element: <Settings />,
  },
  { path: '*', element: <NotFound /> }
])

// 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 实现页面跳转 ​

应用内部的路由跳转使用 <Link>,可以保留当前文档和仍挂载组件的状态。<a href="/..."> 也能导航,但通常会重新加载文档,内存中的 React 状态随之重建;外部链接仍适合使用 <a>。

创建一个简单的临时导航条(下节课我们会升级为完整的 Layout + Sidebar):

tsx
// 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 中的路由配置包裹起来:

tsx
// src/main.tsx (部分修改)
import App from './App'

const router = createBrowserRouter([
  {
    path: '/',
    element: <App />,         // 外层包裹组件
    children: [               // 内部嵌套页面
      { index: true, element: <Home /> },           // 默认子路由 (/)
      { path: 'projects', element: <Projects /> },
      { path: 'projects/:id', element: <ProjectBoard /> },
      { path: 'settings', element: <Settings /> },
      { path: '*', element: <NotFound /> }
    ]
  }
])

深入理解 <Outlet /> ​

在这些子路由之间切换时,父布局通常保持挂载,Outlet 显示匹配的子页面。保持挂载不等于不会重新渲染:布局的状态、props 或所订阅的路由上下文变化时,它仍可能重新渲染。path: '*' 处理未知 URL;实际运行错误应由独立的错误边界处理,不应一律显示成 404。


六、🧠 深度专题:SPA 路由原理 ​

传统的 MPA(多页应用):

  1. 点击 <a href="/about">
  2. 浏览器卸载当前页面,向服务器请求 /about 的 HTML
  3. 服务器返回全新 HTML
  4. 浏览器解析新文档并加载所需资源,可能出现短暂空白

React SPA(单页应用)使用 Client-Side Routing (客户端路由):

这里面最核心的浏览器 API 是 History API:

  • history.pushState(): 改变 URL 而不刷新页面
  • window.addEventListener('popstate', ...): 监听用户点击浏览器的"后退/前进"按钮

pushState() 本身不会触发 popstate。React Router 在自己发起导航时主动更新路由状态,在浏览器前进、后退时再响应 popstate。


七、练习 ​

  1. 在 Projects.tsx 页面中,手动写死三个项目的假数据,并使用 <Link to="/projects/1"> 等链接指向它们。
  2. 体验 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:为静态 SPA 显式配置 rewrite / fallback;不能假定所有部署方式都会自动处理
  • Nginx:需要 try_files $uri $uri/ /index.html;

8.2 路由守卫常见误区 ​

  • 误区 1:只做前端守卫,不做服务端鉴权(可被绕过)
  • 误区 2:把权限判断写死在组件内,导致多处重复
  • 误区 3:未区分“未登录”和“无权限”,造成错误提示混乱

建议:路由层做体验守卫,服务端做最终授权。

8.3 验收标准(L1/L2) ​

  1. L1:实现 4 个页面互跳,并正确高亮当前菜单。
  2. L2:在本地模拟刷新子路由场景,给出 404 修复配置(Vercel/Netlify/Nginx 任一)。

项目驱动 · 边写边学 · React 19