vault backup: 2026-04-25 22:15:39

This commit is contained in:
2026-04-25 22:15:39 +08:00
parent f6ee499fa7
commit 3d8642b7e6
6 changed files with 666 additions and 1 deletions
+135
View File
@@ -0,0 +1,135 @@
---
tags: [frontend, react, react-router, routing, nested-routes]
create time: 2026-04-25 15:30
---
# Outlet:嵌套路由的出口 / 占位符
## 概述
`Outlet` 是 React Router 中用于"嵌套路由占位"的组件,负责在父组件布局中预留一个位置,由子路由匹配到的组件来渲染。
## 正文
### 为什么要用 Outlet
假设有这样的路由结构:
- `/dashboard`
- `/courses`
- `/students`
它们都共享同一个外层布局——左侧导航栏、顶部标题栏、主内容区域。如果不使用 `Outlet`,每个页面组件都要重复写一遍布局结构。
有了 `Outlet`,可以把公共布局抽到一个**父路由组件**中,子页面内容自动渲染到 `Outlet` 位置,不需要重复代码。
> 💡 **想一想**:如果每个子页面都自己写侧边栏和顶栏,当侧边栏需要修改时,需要改几个文件?用了 `Outlet` 后又需要改几个?
### 路由结构对比
```mermaid
graph TB
subgraph "不使用 Outlet"
A1[LoginPage] --> B1[浏览器]
A2[DashboardPage] --> B1
A3[CoursesPage] --> B1
A4[StudentsPage] --> B1
end
subgraph "使用 Outlet"
Layout[Layout 父组件<br/>header + aside + Outlet] --> B2[浏览器]
subgraph "Layout 内部"
H[<header>]
S[<aside>]
O[<Outlet />]
H --> O
S --> O
end
O --> C1[DashboardPage]
O --> C2[CoursesPage]
O --> C3[StudentsPage]
end
```
左边:每个页面独立渲染,布局代码重复。右边:父组件统一管理布局,子页面只关心自己的内容。
### 布局组件实现
```tsx
import { Outlet } from 'react-router-dom'
function Layout() {
return (
<div className="app-layout">
<header>顶部栏</header> {/* 固定不变 */}
<aside>侧边栏</aside> {/* 固定不变 */}
<main>
<Outlet /> {/* 子路由内容渲染到这里 */}
</main>
</div>
)
}
```
**解读**:`Layout` 组件中,`<header>` 和 `<aside>` 是固定结构,始终显示;`<Outlet />` 是一个"占位符",React Router 会自动把当前路径匹配到的子组件渲染到它的位置。
### 路由声明方式
**JSX 声明式写法**:
```tsx
<Route element={<Layout />}> {/* 父路由:负责布局 */}
<Route path="/dashboard" element={<DashboardPage />} />
<Route path="/courses" element={<CoursesPage />} />
<Route path="/students" element={<StudentsPage />} />
</Route>
```
**配置文件写法**:
```ts
const router = createBrowserRouter([
{
path: '/',
element: <Layout />, {/* 父路由组件 */}
children: [ {/* 子路由列表 */}
{ path: 'dashboard', element: <DashboardPage /> },
{ path: 'courses', element: <CoursesPage /> },
{ path: 'students', element: <StudentsPage /> },
],
},
])
```
两种方式等价,选择哪种取决于项目规模和个人偏好。
### Routes vs Outlet
| 特性 | `<Routes>` | `<Outlet />` |
|------|-----------|-------------|
| 职责 | 匹配路径,决定显示哪个组件 | 在父组件中预留子组件渲染位置 |
| 在哪用 | 任意需要路由判断的地方 | 只在父路由组件内部 |
| 类比 | 调度中心 | 出口 / 窗口 |
```tsx
// Routes:路径 → 组件 的映射表
<Routes>
<Route path="/courses" element={<CoursesPage />} />
</Routes>
// Outlet:父组件留给子组件的"位置"
function Layout() {
return (
<div>
<Header />
<Outlet /> {/* 子组件会出现在这里 */}
</div>
)
}
```
> 💡 **想一想**:如果父路由组件里没有放 `<Outlet />`,子路由对应的页面还能显示出来吗?(不能,子组件找不到挂载点,不会被渲染到页面上。)
## 关联笔记
- [[路由原理]] - BrowserRouter、Routes、Route 等核心路由组件
@@ -0,0 +1,203 @@
---
tags:
- 前端
- 依赖管理
- Vite
- React
- TypeScript
create time: 2026-04-25 14:30
---
# 前端项目依赖清单
## 概述
整理在线学习管理后台项目所需的前端依赖包,按功能分类,并给出安装顺序与一句话理解。
## 正文
### 什么是"依赖"
依赖就是你项目需要用到的外部工具包。你不从零造轮子,而是使用社区成熟方案来加速开发。
> **思考:** 为什么我们不做"从零手写路由、手写请求库",而是直接引入现成方案?
>
> 回答:课程项目的核心目标是学习知识,不是重复造轮子。依赖管理是真实开发中的基础能力。
### 核心依赖(必装)
#### 1. 框架层
| 包名 | 作用 | 一句话理解 |
|------|------|-----------|
| `react` | 页面组件开发核心框架 | 写页面、管理状态、组件化 |
| `react-dom` | 将 React 渲染到浏览器 DOM | 没有它页面显示不出来 |
#### 2. 构建工具
| 包名 | 作用 | 一句话理解 |
|------|------|-----------|
| `vite` | 开发服务器 + 热更新 + 生产打包 | 启动和打包项目的施工队 |
```bash
# 创建 Vite + React + TypeScript 项目
npm create vite@latest client -- --template react-ts
cd client
npm install
```
#### 3. 类型系统
| 包名 | 作用 | 一句话理解 |
|------|------|-----------|
| `typescript` | JavaScript 的加强版,提供类型检查 | 提前发现代码错误,减少低级 bug |
```ts
// TypeScript 类型示例:定义学生数据结构
type Student = {
name: string
age: number
class: string
}
// 错误传参会被 TS 编译器直接拒绝
const s: Student = { name: '张三', age: 20, class: '软工2024' }
```
#### 4. UI 组件库
| 包名 | 作用 | 一句话理解 |
|------|------|-----------|
| `antd` | React 成熟组件库(按钮、表格、表单、弹窗等) | 现成的门窗家具,直接拿来用 |
```tsx
import { Button, Input, Table, Modal, Form, Pagination } from 'antd'
// 不需要自己写样式,一行代码就能用
<Button type="primary">新增</Button>
```
#### 5. 样式工具
| 包名 | 作用 | 一句话理解 |
|------|------|-----------|
| `tailwindcss` | 原子化 CSS,直接在 JSX 里写样式类名 | 快速搭页面布局的装修工具 |
```tsx
// 直接写类名控制样式,无需额外 CSS 文件
<div className="flex items-center justify-between p-4 bg-white rounded-lg shadow">
<h2 className="text-lg font-semibold">课程列表</h2>
<Button type="primary">新增</Button>
</div>
```
### 建议安装(按需)
| 包名 | 作用 | 一句话理解 |
|------|------|-----------|
| `react-router-dom` | React 路由,多页面切换 | 各个房间之间的通道 |
| `axios` | HTTP 请求库 | 跟后端通信的电话线 |
| `zustand` | 轻量级状态管理 | 全局状态(登录用户、主题等) |
```tsx
// react-router-dom 示例:定义路由
import { createBrowserRouter, RouterProvider } from 'react-router-dom'
const router = createBrowserRouter([
{ path: '/login', element: <LoginPage /> },
{ path: '/dashboard', element: <DashboardPage /> },
{ path: '/courses', element: <CoursePage /> },
])
```
```ts
// axios 示例:封装请求
import axios from 'axios'
const request = axios.create({
baseURL: '/api',
timeout: 10000,
})
// 统一处理 token 和错误
request.interceptors.request.use((config) => {
const token = localStorage.getItem('token')
if (token) config.headers.Authorization = `Bearer ${token}`
return config
})
```
### 安装顺序(适合初学者)
```bash
# 第一步:创建项目
npm create vite@latest client -- --template react-ts
cd client
npm install
# 第二步:安装 UI 组件库
npm install antd
# 第三步:安装路由和请求
npm install react-router-dom axios
# 第四步:安装 Tailwind CSS(需要额外配置)
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
# 第五步:安装开发类型声明(如果需要)
npm install -D @types/react @types/react-dom
```
### 项目依赖全景图
你可以把这些依赖理解为"盖房子"的过程:
```mermaid
graph LR
subgraph "基础设施"
V[Vite - 施工队]
end
subgraph "主体结构"
R[React - 房子骨架]
RDOM[React-DOM - 落地到浏览器]
end
subgraph "标准与规范"
TS[TypeScript - 图纸标准]
end
subgraph "装修材料"
ANT[Ant Design - 现成组件]
TW[Tailwind CSS - 装修工具]
end
subgraph "配套工程"
RR[React Router - 房间通道]
AX[Axios - 通信线路]
end
V --> R
R --> RDOM
TS -.-> R
TS -.-> RDOM
ANT -.-> R
TW -.-> R
RR -.-> R
AX -.-> R
```
### 依赖关系总结
| 类别 | 必装 | 建议 | 暂不装 |
|------|------|------|--------|
| 框架 | `react`, `react-dom` | - | - |
| 构建 | `vite` | - | - |
| 类型 | `typescript` | - | - |
| UI | `antd` | - | - |
| 样式 | `tailwindcss` | - | - |
| 路由 | - | `react-router-dom` | - |
| 请求 | - | `axios` | - |
| 状态管理 | - | `zustand` | - |
## 关联笔记
- [[登录安全流程]]
- [[验收任务清单]]
- [[题目-在线管理平台]]
@@ -0,0 +1,209 @@
---
tags: [frontend, api, typescript, fetch, abstraction]
create time: 2026-04-25 14:30
---
# 统一封装请求:apiRequest 函数
## 概述
把前端所有接口调用中**重复的逻辑**抽到一个公共函数里,让每个页面只需要关心「调哪个接口、传什么参数」。
> 💡 **想一想**:如果每个页面都自己写一遍 token 拼接、错误处理、JSON 解析,代码会变得怎样?
答案是——重复、易错、难维护。所以业界的标准做法就是:**抽出一个通用请求函数**。
---
## 核心代码
```ts
export async function apiRequest<T>(path: string, init?: RequestInit): Promise<T> {
function getAuthToken() {
return window.localStorage.getItem(TOKEN_STORAGE_KEY) ?? ''
}
const response = await fetch(`${API_BASE}${path}`, {
...init,
headers: {
'Content-Type': 'application/json',
...(getAuthToken() ? { Authorization: `Bearer ${getAuthToken()}` } : {}),
...(init?.headers ?? {}),
},
})
const result = (await response.json()) as ApiResponse<T>
if (!response.ok || result.code !== 200) {
throw new Error(result.msg || '请求失败')
}
return result.data
}
```
---
## 逐行拆解:这段代码在做什么
### 1. 泛型 `<T>` — 让调用方决定返回值类型
```ts
export async function apiRequest<T>(path: string, init?: RequestInit): Promise<T>
```
- `path`:接口路径,比如 `'/users'`
- `init`:fetch 配置项,比如 `method`、`body`
- `<T>`:返回值类型由调用方指定
**好处**:调用时可以自动获得类型提示和类型检查。
### 2. 自动读取 token
```ts
function getAuthToken() {
return window.localStorage.getItem(TOKEN_STORAGE_KEY) ?? ''
}
```
从本地存储取出登录凭证。有 token 就返回 token,没有就返回空字符串。
### 3. 拼接完整请求地址
```ts
const response = await fetch(`${API_BASE}${path}`, { ... })
```
把基础地址和接口路径拼在一起:
| 变量 | 值 |
|------|-----|
| `API_BASE` | `'http://localhost:3000/api'` |
| `path` | `'/login'` |
| **最终 URL** | `'http://localhost:3000/api/login'` |
### 4. 合并请求配置(重点)
```ts
{
...init, // 外部传的配置
headers: {
'Content-Type': 'application/json', // 统一 JSON 格式
...(getAuthToken() ? { Authorization: `Bearer ${getAuthToken()}` } : {}), // 自动加 token
...(init?.headers ?? {}), // 合并外部请求头
},
}
```
层层展开,体现一个设计思想:**统一封装但允许扩展**。
| 优先级 | 顺序 | 说明 |
|--------|------|------|
| 1 | 最低 | `'Content-Type'` 统一设为 JSON |
| 2 | 中间 | 有 token 就自动带上 `Authorization` |
| 3 | 最高 | 调用方自定义的请求头可以覆盖 |
### 5. 统一解析响应
```ts
const result = (await response.json()) as ApiResponse<T>
```
默认后端返回格式为:
```json
{
"code": 200,
"msg": "success",
"data": { ... }
}
```
### 6. 统一判断成功失败
```ts
if (!response.ok || result.code !== 200) {
throw new Error(result.msg || '请求失败')
}
```
做了双重判断:
- **HTTP 层面**:`response.ok` 检查状态码是否在 200~299
- **业务层面**:`result.code === 200` 检查后端返回的业务状态
任意一个失败就抛错,页面层不用再写重复的判断逻辑。
### 7. 只返回业务数据
```ts
return result.data
```
最终直接返回 `data` 字段,调用方拿到的就是最干净的数据。
---
## 请求流程
```mermaid
flowchart LR
A[调用 apiRequest] --> B[读取本地 token]
B --> C[拼接完整 URL]
C --> D[合并请求配置和请求头]
D --> E[发起 fetch 请求]
E --> F[解析 JSON 响应]
F --> G{是否成功?}
G -->|HTTP 或 code 异常| H[抛出错误]
G -->|都正常| I[返回 data]
H --> J[页面层捕获错误]
I --> K[页面层处理业务数据]
```
---
## 实际调用示例
获取课程列表:
```ts
const courses = await apiRequest<Course[]>('/courses')
```
提交表单:
```ts
const user = await apiRequest<User>('/login', {
method: 'POST',
body: JSON.stringify({ username, password }),
})
```
调用方不需要关心:
- token 怎么加
- JSON 怎么解析
- 错误怎么处理
---
## 总结:这段封装的本质
> 把**「请求的共性」**抽出来,变成标准入口;
> 把**「接口的差异」**保留给调用参数。
具体体现在四个层面:
| 层面 | 封装内容 |
|------|----------|
| 函数抽象 | `apiRequest<T>` 统一入口 |
| 配置合并 | headers、URL 自动拼接 |
| 错误统一处理 | HTTP 状态 + 业务 code 双重判断 |
| 返回值格式化 | 只返回 `data`,屏蔽外围结构 |
---
## 关联笔记
- [[路由原理]]
- [[前端项目依赖清单]]
+113
View File
@@ -0,0 +1,113 @@
---
tags: [frontend, react, react-router, routing, spa]
create time: 2026-04-25 15:30
---
# 路由原理:BrowserRouter 与组件协作
## 概述
本文梳理 React Router 的核心组件如何配合完成单页应用(SPA)的路由跳转。重点理解 `<BrowserRouter>` 作为"路由引擎"的作用,以及 `NavLink`、`Routes`、`Route`、`Navigate` 等组件的职责分工。
---
## 正文
### `<BrowserRouter>` 的核心作用
`<BrowserRouter>` 就是 React Router 的**"总开关"和"运行环境"**,没有它,路由组件无法正常工作。
```tsx
<BrowserRouter>
<App />
</BrowserRouter>
```
它做三件事:
1. **提供路由上下文** — 让 `Routes`、`Route`、`NavLink` 知道当前路径是什么、该显示哪个页面
2. **无刷新切换页面** — 基于浏览器的 `History API`,URL 变了但整页不会刷新
3. **全局可用** — 因为它包住了 `<App />`,其所有子组件都能使用路由能力
> 💡 **想一想**:为什么 `<BrowserRouter>` 必须包裹 `<App />` 而不是只包某个子组件?
>
> 如果它只包了 `<Routes>` 但没有包 `<NavLink>`,点击导航按钮会报错:
>
> ```
> Uncaught Error: useHref() may be used only in the context of a <Router> component
> ```
>
> 因为 `NavLink` 内部通过 React Context 读取路由数据,如果它处于 `<BrowserRouter>` 之外,就**读不到任何路由信息**,只能直接崩溃。
### 组件职责类比
| 组件 | 类比角色 | 一句话说明 |
|------|---------|-----------|
| `<BrowserRouter>` | 交通系统 | 提供整个路由的运行环境 |
| `NavLink` | 路牌 / 入口 | 用户点击后触发路径切换 |
| `Routes` | 调度中心 | 读取当前路径,决定走哪条路 |
| `Route` | 目的地 | 定义"路径 → 页面"的映射 |
| `Navigate` | 自动转弯 | 把用户重定向到另一个路径 |
### 路由流程
当用户点击左侧导航栏的"课程管理"时,背后发生了什么?
```mermaid
flowchart LR
A[用户点击 NavLink] --> B[URL 变为 /courses]
B --> C[Routes 读取当前路径]
C --> D{匹配哪个 Route?}
D -->|path=/courses| E[渲染 CoursesPage]
D -->|无匹配| F[不显示任何页面]
E --> G[页面切换完成,整页未刷新]
```
逐帧拆解:
1. `NavLink to="/courses"` 将浏览器地址变为 `/courses`
2. `Routes` 读取当前路径
3. 匹配到 `<Route path="/courses" element={<CoursesPage />} />`
4. `CoursesPage` 组件渲染
整个过程**地址栏变了、组件切换了、但页面没有刷新**。
### 实际代码中的用法
**路由声明**(`App.tsx`):
```tsx
// 根路径自动跳转到 dashboard(replace 不留后退痕迹)
<Route path="/" element={<Navigate to="/dashboard" replace />} />
// 正常路由映射
<Route path="/dashboard" element={<DashboardPage />} />
<Route path="/courses" element={<CoursesPage />} />
```
**导航链接**:
```tsx
<NavLink to="/courses">课程管理</NavLink>
```
**解读**:`<Route path="/" element={<Navigate to="/dashboard" replace />} />` 表示当用户访问根路径 `/` 时,自动跳转到 `/dashboard`。`replace` 参数会让浏览器用新路径替换当前历史记录,这样用户按"后退"按钮不会回到登录页。
### 为什么不能去掉 `<BrowserRouter>`?
路由组件需要"上下文"才知道当前路径是什么、该往哪里跳。如果去掉 `<BrowserRouter>`:
- `NavLink` 不知道点击后该跳到哪里
- `Route` / `Routes` 不知道当前路径是什么
- `Navigate` 无法执行跳转
它们会直接报错。
> 💡 **想一想**:如果我们不用 `<BrowserRouter>`,还有哪些方式能让路由组件知道当前路径?(提示:URL 地址栏本身就是公开的,React Router 还提供了 `HashRouter` 和 `MemoryRouter`,它们分别适用于什么场景?)
---
## 关联笔记
- [[金山办公作业/Week06/登录安全流程]] - 用户认证与 JWT Token
- [[Outlet]] - 嵌套路由的出口 / 占位符
@@ -6,6 +6,8 @@
本作业要求实现一个在线学习管理平台的部分内容,涵盖前端与后端两部分。前端基于 React + TypeScript 技术栈构建,后端采用 Koa 开发。系统实现了用户登录认证、工作台数据可视化、课程管理、学生管理及学习总结展示等功能,支持数据的增删改查等操作。
**相关技术参考**:[[登录安全流程]]、[[路由原理]]
# 二、功能需求
### 1. 登录认证
@@ -57,6 +59,9 @@
### 前端
技术栈:Vite、React、TypeScript、Ant Design、Tailwind CSS 等(禁止使用第三方脚手架如 UmiJS 、飞冰、Ant Design Pro 等同类集成方案)。
参考 ->
1. [[前端项目依赖清单]]
2. [[统一封装请求]]
### 后端
@@ -1,6 +1,6 @@
# 验收任务清单
> 来源:[[题目]]
> 来源:[[题目-在线管理平台]]
## 一、前端功能验收