Skip to content

React Router 路由(二) ​

本页对应 React Router 路由,共 7 段视频,约 1 小时 18 分。

学习目标 ​

  • 能够说明前端路由的作用,并使用 CRA 创建记账本项目环境、安装必要依赖且完成初始化清理。
  • 能够配置 @ 别名路径解析与 VS Code 路径联想,并通过测试导入验证别名可用。
  • 能够使用 json-server 创建 Mock 数据、配置启动命令并验证接口数据返回。
  • 能够根据页面结构划分一级/二级路由,使用 createBrowserRouter、RouterProvider 和 Outlet 完成路由渲染。
  • 能够使用 antd-mobile 的 CSS 变量完成全局/局部主题色定制,并验证组件样式变化。
  • 能够使用 RTK 创建 slice、配置 store、编写异步 action 管理账目列表,并使用 TabBar 组件实现静态布局与点击切换路由。

知识点 ​

先看清页面结构,再决定路由层级 ​

为什么需要先设计路由?因为这个记账本项目不是所有页面都整页切换:点击“月度收支”和“年度账单”时,底部区域不变,只有中间内容变化;点击“记一笔”时,整个页面和路径都变化。路由层级如果一开始分错,后面布局复用、二级出口和跳转都会乱。

怎么想?打开演示站边点边观察:/ 对应月度收支,/new 对应记一笔,这两个应是一级路由;/ 下面还能切到月度、年度,说明月度收支和年度账单是挂在布局容器 Layout 下的二级路由。转写稿里“流由”“red gun routeuter down”“atlet”“pas”等是口误或同音,实际分别是“路由”“react-router-dom”“Outlet”“path”。

怎么做?在 src/router/index.js 中用 createBrowserRouter 创建路由实例,一级配置 Layout 和 New,在 Layout 的 children 中配置 Month、Year。按转写稿整理代码如下:

jsx
import { createBrowserRouter } from 'react-router-dom'
import Layout from '../pages/Layout'
import New from '../pages/New'
import Month from '../pages/Month'
import Year from '../pages/Year'

const router = createBrowserRouter([
  {
    path: '/',
    element: <Layout />,
    children: [
      { path: 'month', element: <Month /> },
      { path: 'year', element: <Year /> }
    ]
  },
  {
    path: '/new',
    element: <New />
  }
])

export default router

易错点:二级路由必须挂在父路由的 children 里;父组件 Layout 中必须放二级出口 <Outlet />,否则访问 /month 时只会看到 Layout,看不到 Month。转写稿的调试过程正是先测 / 和 /new 能显示,再测二级路径发现只显示 Layout,补上 Outlet 后才显示“我是 Month / 我是 Year”。

把路由实例注入应用入口 ​

为什么需要注入?createBrowserRouter 只是创建了路由表,React 应用还不知道它。怎么想?入口文件要使用 RouterProvider 包裹应用,并把 router 传给 router 属性。怎么做?在入口中导入 RouterProvider 和路由实例,把原来的 App 替换掉:

jsx
import { RouterProvider } from 'react-router-dom'
import router from './router'

root.render(
  <RouterProvider router={router} />
)

易错点:忘记导入 RouterProvider、实例没有默认导出、路径写错都会导致页面空白;转写稿中特别提醒“把 app 干掉,渲染 RouterProvider”,本质是让路由接管页面渲染。

用 TabBar 的 onChange 完成路由切换 ​

为什么需要?底部 TabBar 点击后不能只改高亮,还要跳到对应路由。怎么想?看组件文档:TabBar 提供 onChange,点击时把当前项的 path 传出来;拿到 path 后调用 React Router v6 的 useNavigate 跳转。转写稿里“us gate”应纠正为 useNavigate,“陈北”指 TabBar。

怎么做?在布局组件中绑定事件:

jsx
import { useNavigate } from 'react-router-dom'

const navigate = useNavigate()

const switchRoute = (path) => {
  navigate(path)
}

<TabBar onChange={switchRoute} />

数据项里的 key 将来用于路由切换,onChange 回调拿到的就是它。易错点:不要用旧版 useHistory;useNavigate 必须在组件顶层调用;事件参数名要和回调形参对应;路径要和路由表一致,例如 /new、/month、/year,否则点击后匹配不到页面。

调试顺序与常见坑 ​

先启动项目测一级路由:访问 / 看 Layout,访问 /new 看 New;再测二级路由:访问 /month、/year,若只显示父布局,就检查 Outlet。接着在 TabBar 的 onChange 中先 console.log(path),确认点击时能打印出 new、year、month,再调用 navigate(path)。这样能把“事件是否触发”和“路由是否跳转”分开排查。最后注意:项目需安装 react-router-dom v6;二级路径不要重复写父级 /;createBrowserRouter 拼写和导入不能错。转写稿还留了一个思考题:访问 / 时默认显示月度内容,可用索引路由或重定向处理,但本节先保证一级、二级路由和 TabBar 跳转跑通。

先抓住定义:路径与组件的对应关系 ​

本节的核心只有一句话:一个路径 path 对应一个组件 component;当我们在浏览器中访问某个 path 时,这个 path 对应的组件就会在页面中渲染出来。
前端路由中路径与组件对应关系

为什么需要: 在前后端分离的项目里,页面切换不能每次都向后端请求一份新 HTML,否则体验会被打断。前端路由要解决的是:地址变了,页面该显示哪一块内容,由前端自己根据路径来匹配组件。转写稿里的记账本演示很典型:点击“月度收支”和“年度收支”时,底部区域没有整体变化,只是中间内容区在变;点击“记一笔”时,整个页面和路径都变了。这说明路径变化会决定组件的渲染结果,而且组件之间还存在层级关系。

怎么想: 不要一上来就写配置,先观察页面:哪些部分切换时不动,哪些部分跟着路径变。不动的部分通常是父级布局组件,变化的部分是子级路由组件。转写稿分析后得出结论:Layout 是一级路由,它内部承载“月度账单”和“年度账单”两个二级路由;New 是另一个一级路由。这个判断就是前端路由设计的起点。

怎么做: 先建立“路由表”的意识:把 path 和 component 登记成对应关系。访问 / 时渲染 Layout,访问 /new 时渲染 New;在 Layout 的 children 中再登记月度、年度两个二级路径。这样浏览器地址命中哪个 path,就渲染哪个 component。

易错点: 前端路由不是“把组件都写出来再用 CSS 隐藏”,而是按路径匹配渲染;路径和组件必须登记清楚;一级、二级不要凭感觉分,要看页面整体布局是否切换。转写稿里说的“red gun routeuter down”应纠正为 react-router-dom,路由版本是 v6。

二级路由为什么需要出口:Outlet 的作用 ​

为什么需要: 一级路由 Layout 负责整体框架,二级路由只负责框架内部的内容区。如果只在路由表里配了 children,但父组件里没有留渲染位置,二级组件就没有地方显示。

怎么想: 转写稿的调试过程很值得复盘:一级路由能显示,手动输入 new 能显示 New;但输入二级路径时,页面仍只显示 Layout,二级内容不出现。原因不是路由没配,而是 Layout 缺少二级路由出口。

怎么做: 在 Layout 组件中放置 Outlet。转写稿里口述的“atlet”就是 Outlet。父级路由渲染时,Outlet 的位置会渲染当前匹配到的子路由组件。补上后,再访问月度、年度路径,Monthly、Yearly 就能在 Layout 内部显示。

易错点: 二级不显示先查父组件有没有 Outlet;不要把二级路由再配成新的一级布局;Layout、Outlet 这类固定写法不要拼错。转写稿里“le out”应理解为 Layout,“mouse”“页儿”分别对应月度、年度组件。

点击菜单如何触发路由:从事件到跳转 ​

为什么需要: 用户通常不会手输地址,而是点底部 TabBar。点击后要拿到目标路径,再让路由切换。

怎么想: 转写稿的实现思路是:菜单点击时绑定 onChange,回调里先拿到 path 参数;调试时先打印 path,看到点击“记账”打印 new,点击年度、月度分别打印对应路径,确认参数正确后再跳转。这个“先验证参数,再调用跳转”的过程很重要。

怎么做: 用 useNavigate 得到跳转方法,在事件回调中执行 navigate(path)。转写稿里说的“use gate”应纠正为 useNavigate。这样点击哪个菜单,就访问哪个 path,路由匹配后渲染对应组件。

易错点: 不要用 window.location 做跳转,否则容易整页刷新,失去前端路由的意义;事件参数要从菜单数据项的 key/path 来;v6 中不要套用旧版路由的跳转 API。

默认路径与匹配:访问 / 时显示谁 ​

为什么需要: 用户一进来通常访问根路径 /,但根路径下可能需要默认显示“月度账单”,否则内容区为空。

怎么想: 转写稿最后留了思考题:输入 / 时也让月度组件显示。思路是给 Layout 的 children 配一个默认子路由,或把根路径重定向到月度路径。关键是父级 Layout 仍要渲染,二级内容在 Outlet 中出现。

怎么做: 在 children 中配置索引路由或默认重定向到月度路径;保证访问 / 时命中的是 Layout 下的默认子路由,而不是另起一个重复布局。

易错点: 默认子路由不要重复写父级布局;路径斜杠不要多写;匹配不到路径时要有兜底处理。这样,前端路由“路径对应组件渲染”的定义就落到了可运行的页面结构里。

功能演示:先明确三个核心模块 ​

为什么需要:做综合案例前先看成品,避免一上来就写代码却不知道页面之间怎么组织。转写稿中演示了三个核心模块:月度收支、年度收支、新增一笔账单。

怎么想:先按“页面区域是否整体变化”理解需求。月度收支和年度收支主要切换中间内容,新增账单会跳到新的页面,这为后面设计一级、二级路由埋下伏笔。

怎么做:打开演示站点,依次查看:首页月度收支可切换月份;年度账单可切换年份;点击记账进入新增页,选择支出、填写金额、选择餐费并保存后,新记录会回到当月支出列表中。

易错点:不要把“功能演示”当成单纯看效果,要观察路径和布局变化;本节先完成环境准备,不急着实现业务。

创建 CRA 项目并安装依赖 ​

为什么需要:项目要使用路由、状态管理、时间处理、类名处理、移动端组件库和请求库,先把依赖装齐,后续开发才不被环境问题打断。

怎么想:继续使用熟悉的 CRA 创建项目;转写稿明确需要安装:Redux 相关的 @reduxjs/toolkit、react-redux,路由 react-router-dom,时间处理 dayjs,类名处理 classnames,移动端组件库 antd-mobile,请求库 axios。

怎么做:

bash
npx create-react-app react-bill-test
cd react-bill-test
npm i @reduxjs/toolkit react-redux react-router-dom dayjs classnames antd-mobile axios

安装后打开 package.json,在 dependencies 中检查这些包是否都在。转写稿中就是这样逐项确认的。

易错点:包名要写准确,例如是 react-router-dom、antd-mobile、classnames;安装完不要只看终端,要回到 package.json 核对。

清理 CRA 默认文件 ​

为什么需要:CRA 自带样式、logo、测试文件等会干扰案例阅读,先清理成最小入口,后面再逐步添加。

怎么想:保留入口 index.js 和根组件 App.js,删除暂时不用的样式、logo、报告文件和测试文件。转写稿还提到暂时去掉 StrictMode,因为它会让部分钩子执行两次,容易让初学者疑惑。

怎么做:清理 App.css、logo.svg、reportWebVitals.js、测试文件等;index.js 中保留根渲染;App.js 保留一个简单结构。随后执行:

bash
npm start

转写稿中遇到端口占用,终端提示后输入 Y 替换端口即可。

易错点:不要误删 index.js 入口;去掉 StrictMode 只是本案例为减少干扰,不是所有项目都必须删除;启动成功后再提交代码。

配置 @ 别名:路径解析 ​

为什么需要:深层目录中用相对路径会写成很多 ../../,容易出错。配置 @ 指向 src 后,导入更直观。

怎么想:CRA 底层封装了 webpack,默认配置是黑盒,直接改定制化不方便,所以借助 @craco/craco 扩展配置。注意别名分两部分:webpack 的路径解析,以及 VS Code 的路径联想。

怎么做:先安装开发依赖:

bash
npm i -D @craco/craco

在项目根目录新建 craco.config.js:

js
const path = require('path')

module.exports = {
  webpack: {
    alias: {
      '@': path.resolve(__dirname, 'src')
    }
  }
}

再修改 package.json 的启动和打包命令:

json
"scripts": {
  "start": "craco start",
  "build": "craco build"
}

易错点:craco.config.js 必须放在项目根目录;修改 scripts 后仍执行 npm start,但内部已由 craco 接管;别名键 @ 与后面 jsconfig 配置要一致。

配置 @ 别名:VS Code 联想 ​

为什么需要:只配置 webpack 能编译,但编辑器输入 @/ 时没有提示,选择目录仍不方便。

怎么想:这是编辑器层面的配置,不影响打包,只提升开发体验。转写稿明确在根目录新增 jsconfig.json。

怎么做:

json
{
  "compilerOptions": {
    "baseUrl": "./",
    "paths": {
      "@/*": ["src/*"]
    }
  }
}

保存后输入 @/,应能看到 src 下目录的联想提示。可新建 src/test.js 写一个求和函数,在 index.js 中用 import sum from '@/test' 测试;能正常启动、无报错,说明别名生效。

易错点:jsconfig.json 也要放根目录;它只负责联想,真正解析仍靠 craco 的 webpack 配置;两边路径规则要对应。

启动验证与提交 ​

为什么需要:环境配置完成后要确认项目能跑、别名能用,再提交一个干净基线。

怎么想:先验证启动,再验证 @ 导入,最后提交“项目初始化”。

怎么做:

bash
npm start
git add .
git commit -m "项目初始化"

易错点:若别名导入报错,先检查 craco 配置和 scripts 是否改对;提交前确认依赖已安装、页面能正常打开。

课件图示 ​

课件第21页:各个主流框架的路由常用的路由模式有俩种,history模式和hash模式, Re

课件第15页:嵌套路由配置 实现步骤: 1. 使用 children 属性配置路由嵌套关系 2

课件第6页:实际开发中的router配置 page/ Login Article route

课件第17页:当访问的是一级路由时,默认的二级路由组件可以得到渲染,只需要在二级路由的位置去掉

课件第19页:场景:当浏览器输入url的路径在整个路由配置中都找不到对应的 path,为了用户

代码示例 ​

示例 1:路由配置与注入(一级路由 + 嵌套二级路由) ​

jsx
// src/router/index.jsx
import { createBrowserRouter } from 'react-router-dom'
import Layout from '../pages/Layout'
import Month from '../pages/Month'
import Year from '../pages/Year'
import New from '../pages/New'

// 一级路由:Layout(布局容器)、New(记一笔)
// 二级路由:Layout 内部的 Month(月度账单)、Year(年度账单)
const router = createBrowserRouter([
  {
    path: '/',
    element: <Layout />,
    children: [
      { path: '', element: <Month /> },      // 访问 / 时渲染月度账单
      { path: 'year', element: <Year /> }    // 访问 /year 时渲染年度账单
    ]
  },
  {
    path: '/new',
    element: <New />
  }
])

export default router
jsx
// src/pages/Layout/index.jsx
import { Outlet } from 'react-router-dom'

const Layout = () => {
  return (
    <div>
      <div className="container">
        {/* 二级路由的出口,Month / Year 在这里渲染 */}
        <Outlet />
      </div>
      <div className="bottom">
        {/* TabBar 见示例 2 */}
      </div>
    </div>
  )
}

export default Layout
jsx
// src/index.js
import ReactDOM from 'react-dom/client'
import { RouterProvider } from 'react-router-dom'
import router from './router'

const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(<RouterProvider router={router} />)

要点解释

  • createBrowserRouter([...]) 接收一个路由对象数组,path 与 element 一一对应;返回的 router 实例必须通过 <RouterProvider router={router} /> 注入,组件内部才能使用路由相关 Hook。
  • 二级路由不是另起一套配置,而是写在父路由对象的 children 里;父级 path 为 /,子级写相对路径('' 表示 index,'year' 表示 /year)。
  • 易错点:只配了 children 但父组件里忘记写 <Outlet />,二级路由匹配成功却渲染不出来——现象是访问 /year 时页面仍只显示“我是 Layout”。
  • 易错点:Outlet 要放在希望二级内容出现的位置(这里在底部 TabBar 之上),位置放错会导致布局错乱。
  • 易错点:createBrowserRouter 的对象数组里,element 必须是 JSX(<Layout />),写成 Layout 函数名不会渲染。
  • 开发阶段如果把 <StrictMode> 包在入口,Effect 会执行两次,调试时容易误以为接口被请求了两遍,可暂时去掉。

示例 2:TabBar 标签栏 + 点击切换路由 ​

jsx
// src/pages/Layout/index.jsx
import { Outlet, useNavigate } from 'react-router-dom'
import { TabBar } from 'antd-mobile'
import { AppOutline, CalendarOutline, AddCircleOutline } from 'antd-mobile-icons'
import './index.css'

// key 就是将来要跳转的路由 path,必须和路由表里的 path 对应
const tabs = [
  { key: '/', title: '月度账单', icon: <AppOutline /> },
  { key: '/year', title: '年度账单', icon: <CalendarOutline /> },
  { key: '/new', title: '记一笔', icon: <AddCircleOutline /> }
]

const Layout = () => {
  const navigate = useNavigate()

  // 点击菜单,拿到当前项的 key(即 path),做路由跳转
  const switchRoute = (path) => {
    navigate(path)
  }

  return (
    <div className="layout">
      <div className="container">
        <Outlet />
      </div>
      <div className="bottom">
        <TabBar onChange={switchRoute}>
          {tabs.map((item) => (
            <TabBar.Item
              key={item.key}
              icon={item.icon}
              title={item.title}
            />
          ))}
        </TabBar>
      </div>
    </div>
  )
}

export default Layout
css
/* src/pages/Layout/index.css */
html,
body,
#root {
  height: 100%;
}

.layout {
  height: 100%;
  display: flex;
  flex-direction: column;
}

/* 二级路由内容区占满剩余空间,超出可滚动 */
.container {
  flex: 1;
  overflow-y: auto;
}

/* TabBar 固定在底部 */
.bottom {
  flex-shrink: 0;
  border-top: 1px solid #eee;
}

要点解释

  • TabBar 的 onChange 在点击切换时自动触发,回调参数就是当前 TabBar.Item 的 key,所以 tabs 里把 key 直接写成路由 path,拿到后 navigate(path) 即可跳转。
  • 易错点:key 写成 'year'(少了 /)或 'month' 之类的随意值,navigate 会按相对路径跳转,导致地址变成 /year/year 这类错误路径。
  • 易错点:useNavigate 只能在被 RouterProvider 包裹的组件内使用,Layout 必须是被路由渲染出来的组件(element: <Layout />),否则报“useNavigate 只能在 Router 上下文中使用”。
  • tabs.map 时 key 必须唯一且不可省略,否则控制台报 warning,切换时也可能渲染错位。
  • 布局上用 flex 让内容区 flex: 1 自适应、TabBar flex-shrink: 0 固定在底部,避免内容把 TabBar 顶出视口。
  • 使用 antd-mobile 的样式依赖,需要额外安装 antd-mobile 与 antd-mobile-icons,并在入口引入全局样式,否则组件样式不生效。

示例 3:@ 别名路径配置(路径解析 + 路径联想) ​

js
// craco.config.js  —— 必须放在项目根目录,文件名固定
const path = require('path')

module.exports = {
  webpack: {
    alias: {
      // 把 @ 解析为 src 目录
      '@': path.resolve(__dirname, 'src')
    }
  }
}
json
// jsconfig.json —— 放在项目根目录,供 VS Code 做路径联想
{
  "compilerOptions": {
    "baseUrl": "./",
    "paths": {
      "@/*": ["src/*"]
    }
  }
}
json
// package.json 片段:用 craco 接管原来的启动与打包命令
{
  "scripts": {
    "start": "craco start",
    "build": "craco build"
  }
}
jsx
// src/index.js —— 验证别名是否生效
import { sum } from '@/test'

console.log(sum(1, 2))

要点解释

  • CRA 把 webpack 配置封装成黑盒,想改 alias 必须借助 @craco/craco 这个扩展配置插件(装在开发依赖 -D),配置文件名字必须是 craco.config.js。
  • 易错点:craco.config.js 和 jsconfig.json 都必须放在项目根目录,放到 src 里不会生效。
  • 易错点:只建了 craco.config.js 却没改 package.json 里的 start / build 命令,依旧执行 react-scripts start,别名完全不生效。
  • jsconfig.json 只负责 VS Code 的联想提示,craco.config.js 才负责真正的编译解析,两者缺一不可:少前者没提示,少后者编译报“找不到模块”。
  • path.resolve(__dirname, 'src') 中 __dirname 是当前配置文件所在目录(即项目根目录),直接写 './src' 容易因相对路径基准不同而出错。

常见错误 ​

1. 配完 @ 别名,代码里 import xxx from '@/xxx' 仍然报"找不到模块"

  • 现象:编辑器不报红但运行报错,或两边都报错;npm start 直接失败。
  • 原因:三个环节任一缺失——craco.config.js 没放在项目根目录(放到 src 里最常见);package.json 的 start/build 没换成 craco start / craco build;改完没重启 dev server(webpack 配置只在启动时读取一次)。
  • 修法:确认根目录下有 craco.config.js;核对 scripts 已被 craco 接管;杀掉进程重新 npm start;用一次真实的 @/ 引用跑通再继续写业务。

2. VS Code 里敲 @/ 没有任何路径联想,但项目能跑

  • 现象:编译正常,只是没有自动补全,写路径全靠手打。
  • 原因:把"路径解析"和"路径联想"当成一件事——前者是 webpack(craco)的事,后者是编辑器读 jsconfig.json 的事;或 jsconfig.json 位置不对、baseUrl/paths 写错、通配符 * 漏写。
  • 修法:根目录新建 jsconfig.json,baseUrl 设为当前目录,paths 里写 "@/*": ["src/*"];保存后若仍无提示,重启 VS Code 窗口(编辑器只在启动时加载该配置)。

3. 二级路由能匹配上,但页面只渲染布局容器,子页面内容是空的

  • 现象:访问 /month、/year 时地址栏对了,Layout 里的按钮、底部 TabBar 都在,中间区域一片空白。
  • 原因:Layout 组件里漏写 <Outlet />;或子路由 children 的 path 写成了绝对路径(/month)而非相对路径(month),导致嵌套关系失效。
  • 修法:在 Layout 的内容区补 <Outlet />;子路由 path 一律写相对值;用"改地址栏 + 点 TabBar"两种方式各验证一次。

4. 启动即报 name is a required option for createSlice

  • 现象:页面白屏,控制台报错,整条 Redux 链路全断。
  • 原因:createSlice 的配置对象里漏了 name 字段(写业务时只顾着写 initialState 和 reducers)。
  • 修法:补上 name;顺带检查另外两个必填项 initialState、reducers 是否齐全。改完刷新,先看 Redux DevTools 里有没有对应 slice,再往下写组件。

5. 组件里 dispatch(getBillList()) 报 "is not a function"

  • 现象:异步 action 拿不到,或者解构出来是 undefined。
  • 原因:createSlice 返回值解构时写错字段名——是 actions(带 s),不是 action;同理 reducer 是单数。
  • 修法:统一记住 const { actions, reducer } = slice;解构后立即在下面 dispatch 一次自测,确认函数存在再往下写业务。

6. 组件库主题色改了,但按钮还是默认蓝

  • 现象:样式文件写了 CSS 变量,页面无变化。
  • 原因:样式文件没在入口 import(写了但没加载);变量名拼错(组件库的变量名是固定契约,不能自造);做局部定制时,外层类名和实际渲染出的 DOM 类名对不上。
  • 修法:先在入口文件确认 import 生效(可在浏览器里看该样式是否被加载);变量名从官方文档直接复制;局部定制时用 DevTools 确认外层元素的真实类名。

7. mock 接口 404 或返回的不是预期数组

  • 现象:axios.get 拿到 404,或拿到 {} 导致列表渲染报错。
  • 原因:启动命令里的 json 文件路径写错(相对路径是相对执行命令时的当前目录);端口被占用换端口后前端地址没同步改;json 顶层结构被改成了对象而代码按数组取。
  • 修法:先在浏览器直接访问接口地址确认返回结构,再回代码里写;把接口地址抽成常量,改端口只改一处。

8. TabBar 点击后高亮和路由对不上,或从记账页返回时回退路径混乱

  • 现象:点"记账"再返回,回到的是上上次的页面;或某个 tab 永远不高亮。
  • 原因:onChange 回调里拿到的 key 与 path 没做对应;跳转用了默认的 push,导致历史栈里堆了一串中间态。
  • 修法:先在回调里 console.log 打印 key,确认它就是路由 path;跳转时按需加 { replace: true };高亮态用 useLocation().pathname 反推,而不是自己维护一份 state。

练习 ​

1. 默认路由兜底 访问 / 时直接展示月度账单内容,且地址栏仍保持 /。要求:用路由配置解决,不要在 Layout 里写 useEffect 手动跳转。(提示:想一想 index 路由和 Navigate 两种做法,说说各自在"地址栏显示"上的差别。)

2. 多级路径下的 TabBar 高亮 把路由改成 /bill/month、/bill/year,再让底部 TabBar 在进入 /bill/new 这类更深层级时也能正确高亮/不高亮。要求:写出你的匹配规则,并说明为什么不能用 pathname === path 直接比较。

3. 串起一条写链路 新增一个 addBill 异步 action:POST 到 mock 服务保存一笔账单,成功后重新拉取列表并让首页刷新。要求:失败时给出提示,不要在组件里直接 axios。

4. 一条命令启动两套服务 把 mock 服务和前端服务合并成 npm start 一条命令,同时保证任意一方启动失败时你能立刻发现。要求:说明你用的方式(&、npm-run-all 或 concurrently)在 Windows / macOS 上的差异,以及日志混在一起时你怎么定位问题。

5. 局部主题定制 在不动全局主题的前提下,只让"记账"页面里的按钮变成另一套配色。要求:给出样式写法,并在 DevTools 里验证该样式的作用范围没有溢出到别的页面。

原视频对照 ​

#视频时长
1记账本-功能演示和环境创建09:12
2记账本-配置别名路径@12:13
3记账本-数据Mock实现08:56
4记账本-整体路由设计10:12
5记账本-antD主题色定制11:08
6记账本-Redux管理账目列表16:26
7记账本-TabBar功能实现10:33

小结 ​

  1. @ 别名是两件事:webpack 侧负责"编译能找到文件",编辑器侧负责"写的时候有提示"。只配一半,项目照样能跑,但开发体验会持续掉血。
  2. 改构建配置的三件套是固定的——配置文件在根目录、启动命令被接管、改完必须重启。踩坑几乎都出在漏掉其中一条。
  3. 路由层级不要拍脑袋定,看"变化区域":整个页面换 = 一级,只有局部内容换 = 二级;二级一定要有 <Outlet /> 作为出口,否则匹配成功但渲染为空。
  4. 组件库的正确姿势是先抄 demo 跑通、再改定制。跑不通时先怀疑依赖没装(如 sass)或样式没引入,而不是怀疑组件本身。
  5. RTK 的写法高度模板化,name / initialState / reducers、actions(带 s)、reducer(单数)这些固定项错一个就整条链路断,写完先看 DevTools 再写组件。
  6. 调试的顺序应该是从外到内:先看 Network 拿到数据没有,再看 Redux 里存进去没有,最后才看组件渲染——跳过前两步直接改 JSX 是最耗时的做法。
  7. 项目里的重复手工操作(两个窗口、两条命令、两处地址)都值得在当天就合并掉,它省的不是这一次的时间,是后面每一次忘记执行时的排查时间。