React

1. React 基础

1) 环境准备

创建项目

首先,通过 react 脚手架创建项目

npx create-react-app client --template typescript
  • client 是项目名
  • 目前 react 版本是 18.x

运行项目

cd client
npm start
  • 会自动打开浏览器,默认监听 3000 端口

在这里插入图片描述

修改端口

在项目根目录下新建文件 .env.development,它可以定义开发环境下的环境变量

PORT=7070

重启项目,端口就变成了 7070

  • 参考文档:Advanced Configuration | Create React App (create-react-app.dev)
浏览器插件

插件地址 New React Developer Tools – React Blog (reactjs.org)

在这里插入图片描述

VSCode

推荐安装 Prettier 代码格式化插件

在这里插入图片描述

2) 入门案例

Hello

编写一个 src/pages/Hello.tsx 组件

export default function Hello()  {return <h3>Hello, World!</h3>
}
  • 组件中使用了 jsx 语法,即在 js 中直接使用 html 标签或组件标签
  • 函数式组件必须返回标签片段

在 index.js 引入组件

import React from 'react'
import ReactDOM from 'react-dom/client'
import './index.css'
import reportWebVitals from './reportWebVitals'
// 1. 引入组件
import Hello from './pages/Hello'const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)
root.render(<React.StrictMode>{/* 2. 将原来的 <App/> 改为 <Hello></Hello> */}<Hello></Hello></React.StrictMode>
)// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals()

将欢迎词作为属性传递给组件

<Hello msg='你好'></Hello>
  • 字符串值,可以直接使用双引号赋值
  • 其它类型的值,用 {值}

而组件修改为

export default function Hello(props: { msg: string }) {return <h3>{props.msg}</h3>
}
jsx 原理
export default function Hello(props: { msg: string }) {return <h3>{props.msg}</h3>
}

在 v17 之前,其实相当于

import { createElement } from "react";
export default function Hello(props: {msg: string}) {return createElement('h3', null, `${props.msg}`)
}

3) 人物卡片案例

样式已经准备好 /src/css/P1.css

#root {display: flex;width: 100vw;height: 100vh;justify-content: center;align-items: center;
}div.student {flex-shrink: 0;flex-grow: 0;position: relative;width: 128px;height: 330px;/* font-family: '华文行楷'; */font-size: 14px;text-align: center;margin: 20px;display: flex;justify-content: flex-start;background-color: #7591AD;align-items: center;flex-direction: column;border-radius: 5px;box-shadow: 0 0 8px #2c2c2c;color: #e8f6fd;
}.photo {position: absolute;width: 100%;height: 100%;top: 0;border-radius: 0%;overflow: hidden;transition: 0.3s;border-radius: 5px;
}.photo img {width: 100%;height: 100%;/* object-fit: scale-down; */object-fit: cover;
}.photo::before {position: absolute;content: '';width: 100%;height: 100%;background-image: linear-gradient(to top, #333, transparent);
}div.student h2 {position: absolute;font-size: 20px;width: 100%;height: 68px;font-weight: normal;text-align: center;margin: 0;line-height: 68px;visibility: hidden;
}h2::before {position: absolute;top: 0;left: 0;content: '';width: 100%;height: 68px;background-color: rgba(0, 0, 0, 0.3);border-top-left-radius: 5px;border-top-right-radius: 5px;  
}div.student h1 {position: absolute;top: 250px;font-size: 22px;margin: 0;transition: 0.3s;font-weight: normal;
}div.student p {margin-top: 300px;width: 80%;font-weight: normal;text-align: center;padding-bottom: 5px;border-bottom: 1px solid #8ea2b8;
}.student:hover .photo::before {display: none;
}.student:hover .photo {width: 90px;height: 90px;top: 90px;border-radius: 50%;box-shadow: 0 0 15px #111;
}.student:hover img {object-position: 50% 0%;
}.student:hover h1 {position: absolute;top: 190px;width: 40px;
}div.student:hover h2 {visibility: visible;
}

类型 /src/model/Student.ts

export interface Student {id: number,name: string,sex: string,age: number,photo: string
}

组件 /src/pages/P1.tsx

import { Student } from '../model/Student'
import '../css/P1.css'
export default function P1(props: { student: Student }) {return (<div className='student'><div className='photo'><img src={props.student.photo}/></div><h1>{props.student.name}</h1><h2>{props.student.id}</h2>      <p>性别 {props.student.sex} 年龄 {props.student.age}</p></div>)
}

使用组件

const stu1 = { id: 1, name: '风清扬', sex: '男', age: 99, photo: '/imgs/1.png' }
const stu2 = { id: 2, name: '玮哥', sex: '男', age: 20, photo: '/imgs/2.png' }
const stu3 = { id: 3, name: '长宏', sex: '男', age: 30, photo: '/imgs/3.png'}<P1 student={stu1}></P1>
<P1 student={stu2}></P1>
<P1 student={stu3}></P1>
路径
  • src 下的资源,要用相对路径引入
  • public 下的资源,记得 / 代表路径的起点
标签命名
  • 组件标签必须用大驼峰命名
  • 普通 html 标签必须用小写命名
事件处理
import { Student } from '../model/Student'
import '../css/P1.css'
export default function P1(props: { student: Student }) {function handleClick(e : React.MouseEvent){console.log(student)console.log(e)}return (<div className='student'><div className='photo' onClick={handleClick}><img src={props.student.photo}/></div><h1>{props.student.name}</h1><h2>{props.student.id}</h2><p>性别 {props.student.sex} 年龄 {props.student.age}</p></div>)
}
  • 事件以小驼峰命名
  • 事件处理函数可以有一个事件对象参数,可以获取事件相关信息
列表 & Key
import { Student } from '../model/Student'
import P1 from './P1'export default function P2(props: { students: Student[] }) {return (<>{props.students.map((s) => ( <P1 student={s} key={s.id}></P1> ))}</>)
}
  • key 在循环时是必须的,否则会有 warning

也可以这么做

import { Student } from '../model/Student'
import P1 from './P1'export default function P2(props: { students: Student[] }) {const list = props.students.map((s) => <P1 student={s} key={s.id}></P1>)return <>{list}</>
}

使用组件

const stu1 = { id: 1, name: '风清扬', sex: '男', age: 99, photo: '/1.png' }
const stu2 = { id: 2, name: '玮哥', sex: '男', age: 45, photo: '/2.png' }
const stu3 = { id: 3, name: '长宏', sex: '男', age: 45, photo: '/3.png'}<P2 students={[stu1,stu2,stu3]}></P2>
条件渲染

P1 修改为

import { Student } from '../model/Student'
import '../css/P1.css'
export default function P1(props: { student: Student; hideAge?: boolean }) {function handleClick() {console.log(props.student)}const ageFragment = !props.hideAge && <span>年龄 {props.student.age}</span>return (<div className='student'><div className='photo' onClick={handleClick}><img src={props.student.photo} /></div><h1>{props.student.name}</h1><h2>{props.student.id}</h2><p>性别 {props.student.sex} {ageFragment}</p></div>)
}
  • 子元素如果是布尔值,nullish,不会渲染

P2 修改为

import { Student } from '../model/Student'
import P1 from './P1'export default function P2(props: { students: Student[]; hideAge?: boolean }) {const list = props.students.map((s) => (<P1 student={s} hideAge={props.hideAge} key={s.id}></P1>))return <>{list}</>
}

使用组件

const stu1 = { id: 1, name: '风清扬', sex: '男', age: 99, photo: '/1.png' }
const stu2 = { id: 2, name: '玮哥', sex: '男', age: 45, photo: '/2.png' }
const stu3 = { id: 3, name: '长宏', sex: '男', age: 45, photo: '/3.png'}<P2 students={[stu1,stu2,stu3]} hideAge={true}></P2>
参数解构

以 P1 组件为例

import { Student } from '../model/Student'
import '../css/P1.css'
export default function P1
({ student, hideAge = false }: { student: Student, hideAge?: boolean }) {function handleClick() {console.log(student)}const ageFragment = !hideAge && <span>年龄 {student.age}</span>return (<div className='student'><div className='photo' onClick={handleClick}><img src={student.photo} /></div><h1>{student.name}</h1><h2>{student.id}</h2><p>性别 {student.sex} {ageFragment}</p></div>)
}
  • 可以利用解构赋值语句,让 props 的使用更为简单
  • 对象解构赋值还有一个额外的好处,给属性赋默认值

使用组件

const stu1 = { id: 1, name: '风清扬', sex: '男', age: 99, photo: '/1.png' }<P1 student={stu1}></P1>

4) 处理变化的数据

入门案例侧重的是数据展示,并未涉及到数据的变动,接下来我们开始学习 react 如何处理数据变化

axios

首先来学习 axios,作用是发送请求、接收响应,从服务器获取真实数据

安装

npm install axios

定义组件

import axios from 'axios'
export default function P4({ id }: { id: number }) {async function updateStudent() {const resp = await axios.get(`http://localhost:8080/api/students/${id}`)console.log(resp.data.data)}updateStudent()return <></>
}
  • 其中 /api/students/${id} 是提前准备好的后端服务 api,会延迟 2s 返回结果

使用组件

<P4 id={1}></P4>

在控制台上打印

{"id": 1,"name": "宋远桥","sex": "男","age": 40
}

当属性变化时,会重新触发 P4 组件执行,例如将 id 从 1 修改为 2

在这里插入图片描述

执行流程

  • 首次调用函数组件,返回的 jsx 代码会被渲染成【虚拟 dom 节点】(也称 Fiber 节点)
    • 根据【虚拟 dom 节点】会生成【真实 dom 节点】,由浏览器显示出来
  • 当函数组件的 props 或 state 发生变化时,才会重新调用函数组件,返回 jsx
    • jsx 与上次的【虚拟 dom 节点】对比
      • 如果没变化,复用上次的节点
      • 有变化,创建新的【虚拟 dom 节点】替换掉上次的节点
  • 由于严格模式会触发两次渲染,为了避免干扰,请先注释掉 index.tsx 中的 <React.StrictMode>
状态

先来看一个例子,能否把服务器返回的数据显示在页面上

import axios from 'axios'
let count = 0
export default function P5(props: { id: number }) {function getTime() {const d = new Date()return d.getHours() + ':' + d.getMinutes() + ':' + d.getSeconds()}async function updateStudent() {const resp = await axios.get(`http://localhost:8080/api/students/${props.id}`)Object.assign(student, resp.data.data)console.log(current, student, getTime())}const current = count++let student = { name: 'xx' }console.log(current, student, getTime())updateStudent()return <h3>姓名是:{student.name}</h3>
}
  • count 是一个全局变量,记录 P5 函数第几次被调用

执行效果,控制台上

0 {name: 'xx'} '16:22:16'
0 {id: 1, name: '宋远桥', sex: '男', age: 40} '16:22:18'

此时页面仍显示 姓名是:xx

那么修改一下 props 的 id 呢?进入开发工具把 id 从 1 修改为 2,控制台上

1 {name: 'xx'} '16:24:0'
1 {id: 2, name: '俞莲舟', sex: '男', age: 38} '16:24:2'

此时页面仍显示 姓名是:xx

为什么页面没有显示两秒后更新的值?

  • 第一次,页面显示的是 P5 函数的返回结果,这时 student.name 还未被更新成宋远桥,页面显示 xx
    • 虽然 2s 后数据更新了,但此时并未触发函数执行,页面不变
  • 第二次,虽然 props 修改触发了函数重新执行,但既然函数重新执行,函数内的 student 又被赋值为 { name: 'xx' },页面还是显示 xx
    • 2s 后数据更新,跟第一次一样,并未重新触发函数执行,页面不变

结论:

  • 函数是无状态的,执行完毕后,它内部用的数据都不会保存下来
  • 要想让函数有状态,就需要使用 useState 把数据保存在函数之外的地方,这些数据,称之为状态
useState
import axios from 'axios'
import { useReducer, useState } from 'react'
import { Student } from '../model/Student'
let count = 0
export default function P5(props: { id: number }) {// ...async function updateStudent() {const resp = await axios.get(`http://localhost:8080/api/students/${props.id}`)Object.assign(student, resp.data.data)console.log(current, student, getTime())}const current = count++let [student] = useState<Student>({ name: 'xx' })console.log(current, student, getTime())updateStudent()return <h3>姓名是:{student.name}</h3>
}

接下来使用 setXXX 方法更新 State

import axios from 'axios'
import { useState } from 'react'
import { Student } from '../model/Student'
export default function P5(props: { id: number }) {async function updateStudent() {const resp = await axios.get(`/api/students/${props.id}`)setStudent(resp.data.data)}let [student, setStudent] = useState<Student>({ name: 'xx' })updateStudent()return <h3>姓名是:{student.name}</h3>
}

工作流程如下

首次使用 useState,用它的参数初始化 State

在这里插入图片描述

2s 后数据更新,setStudent 函数会更新 State 数据,并会触发下一次渲染(P5 的调用)

在这里插入图片描述

再次调用 useState,这时返回更新后的数据

在这里插入图片描述

这时再返回 jsx,内容就是 姓名是:宋远桥

P.S.

使用了 useState 之后,会执行两次 xhr 请求,后一次请求是 react 开发工具发送的,不用理会

问题还未结束,第二次 P5 调用时,updateStudent 还会执行,结果会导致 2s 后响应返回继续调用 setStudent,这会导致每隔 2s 调用一次 P5 函数(渲染一次)

在这里插入图片描述

如何让 updateStudent 只执行一次呢?一种土办法是再设置一个布尔 State

在这里插入图片描述

接下来数据更新

在这里插入图片描述

第二次进入 P5 函数时,由于 fetch 条件不成立,因此不会再执行两个 setXXX 方法

在这里插入图片描述

函数式组件的工作流程

  • 首次调用函数组件,返回的 jsx 代码会被渲染成【虚拟 dom 节点】(也称 Fiber 节点)
    • 此时使用 useState 会将组件工作过程中需要数据绑定到【虚拟 dom 节点】上
    • 根据【虚拟 dom 节点】会生成【真实 dom 节点】,由浏览器显示出来
  • 当函数组件的 props 或 state 发生变化时,才会重新调用函数组件,返回 jsx
    • props 变化由父组件决定,state 变化由组件自身决定
    • jsx 与上次的【虚拟 dom 节点】对比
      • 如果没变化,复用上次的节点
      • 有变化,创建新的【虚拟 dom 节点】替换掉上次的节点
useEffect

Effect 称之为副作用(没有贬义),函数组件的主要目的,是为了渲染生成 html 元素,除了这个主要功能以外,管理状态,fetch 数据 … 等等之外的功能,都可以称之为副作用。

useXXX 打头的一系列方法,都是为副作用而生的,在 react 中把它们称为 Hooks

useEffect 三种用法

import axios from "axios"
import { useEffect, useState } from "react"/*
useEffect参数1:箭头函数, 在真正渲染 html 之前会执行它参数2:情况1:没有, 代表每次执行组件函数时, 都会执行副作用函数情况2:[], 代表副作用函数只会执行一次情况3:[依赖项], 依赖项变化时,副作用函数会执行
*/
export default function P6({ id, age }: { id: number, age: number }) {console.log('1.主要功能')// useEffect(() => console.log('3.副作用功能')) // useEffect(() => console.log('3.副作用功能'), []) useEffect(() => console.log('3.副作用功能'), [id]) console.log('2.主要功能')return <h3>{id}</h3>
}

用它改写 P5 案例

import axios from "axios"
import { useEffect, useState } from "react"export default function P6({ id, age }: { id: number, age: number }) {const [student, setStudent] = useState({name:'xx'})useEffect(()=>{async function updateStudent() {const resp = await axios.get(`http://localhost:8080/api/students/${id}`)    setStudent(resp.data.data)}updateStudent()}, [id])return <h3>{student.name}</h3>
}
useContext
import axios from 'axios'
import { createContext, useContext, useEffect, useState } from 'react'
import { R, Student } from '../model/Student'/*createContext         创建上下文对象useContext            读取上下文对象的值<上下文对象.Provider>  修改上下文对象的值
*/
const HiddenContext = createContext(false)// 给以下组件提供数据,控制年龄隐藏、显示
export default function P7() {const [students, setStudents] = useState<Student[]>([])const [hidden, setHidden] = useState(false)useEffect(()=>{async function updateStudents() {const resp = await axios.get<R<Student[]>>("http://localhost:8080/api/students")setStudents(resp.data.data)}updateStudents()}, [])function hideOrShow() {// 参数:上一次状态值,旧值// 返回值:要更新的新值setHidden((old)=>{return !old})}return <HiddenContext.Provider value={hidden}><input type="button" value={hidden?'显示':'隐藏'} onClick={hideOrShow}/><P71 students={students}></P71></HiddenContext.Provider>  
}// 负责处理学生集合
function P71({ students }: { students: Student[] }) {const list = students.map(s=><P72 student={s} key={s.id}></P72>)return <>{list}</>
}// 负责显示单个学生
function P72({ student }: { student: Student }) {const hidden = useContext(HiddenContext)const jsx = !hidden && <span>{student.age}</span>return <div>{student.name} {jsx}</div>
}
  • 如果组件分散在多个文件中,HiddenContext 应该 export 导出,用到它的组件 import 导入
  • React 中因修改触发的组件重新渲染,都应当是自上而下的
  • setHidden 方法如果更新的是对象,那么要返回一个新对象,而不是在旧对象上做修改
表单
import axios from 'axios'
import React, { useState } from 'react'
import '../css/P8.css'export default function P8() {const [student, setStudent] = useState({name:'', sex:'男', age:18})const [message, setMessage] = useState('')const options = ['男', '女']const jsx = options.map(e => <option key={e}>{e}</option>)// e 事件对象, e.target 事件源function onChange(e : React.ChangeEvent<HTMLInputElement|HTMLSelectElement>) {setStudent((old)=>{// 返回的新值,不能与旧值是同一个对象return {...old, [e.target.name]:e.target.value}})}async function onClick() {const resp = await axios.post('http://localhost:8080/api/students', student)setMessage(resp.data.data)}const messageJsx = message && <div className='success'>{message}</div>return (<form><div><label>姓名</label><input type="text" value={student.name} onChange={onChange} name='name'/></div><div><label>性别</label><select value={student.sex} onChange={onChange} name='sex'>{jsx}</select></div><div><label>年龄</label><input type="text" value={student.age} onChange={onChange} name='age' /></div><div><input type='button' value='新增' onClick={onClick}/></div>{messageJsx}</form>)
}

2. React 进阶

1) Ant Design

react 组件库

  • 官网地址:https://ant.design/

  • 镜像地址1:https://ant-design.gitee.io/

  • 镜像地址2:https://ant-design.antgroup.com/

入门

安装

npm install antd
  • 目前版本是 4.x

引入样式,在 css 文件中加入

@import '~antd/dist/antd.css';

引入 antd 组件

import { Button } from "antd";export default function A1() {return <Button type='primary'>按钮</Button>
}
国际化

试试其它组件

import { Button, Modal } from "antd";export default function A1() {return <Modal open={true} title='对话框'>内容</Modal>
}

发现确定和取消按钮是英文的,这是因为 antd 支持多种语言,而默认语言是英文

要想改为中文,建议修改最外层的组件 index.tsx

// ...
import { ConfigProvider } from 'antd'
import zhCN from 'antd/es/locale/zh_CN'root.render(<ConfigProvider locale={zhCN}><A1></A1></ConfigProvider>
)
表格
import { Table } from 'antd'
import { ColumnsType } from 'antd/lib/table'
import axios from 'axios'
import { useEffect, useState } from 'react'
import { R, Student } from '../model/Student'export default function A3() {const [students, setStudents] = useState<Student[]>([])const [loading, setLoading] = useState(true)useEffect(() => {async function getStudents() {const resp = await axios.get<R<Student[]>>('http://localhost:8080/api/students')setStudents(resp.data.data)setLoading(false)}getStudents()}, [])// title: 列标题  dataIndex: 要关联的属性名const columns: ColumnsType<Student> = [{title: '编号',dataIndex: 'id',},{title: '姓名',dataIndex: 'name',},{title: '性别',dataIndex: 'sex',},{title: '年龄',dataIndex: 'age',},]// columns: 列定义// dataSource: 数据源,一般是数组包对象// rowKey: 作为唯一标识的属性名// loading: 显示加载图片return (<Tablecolumns={columns}dataSource={students}rowKey='id'loading={loading}></Table>)
}
客户端分页
import { Table } from 'antd'
import { ColumnsType, TablePaginationConfig } from 'antd/lib/table'
import axios from 'axios'
import { useEffect, useState } from 'react'
import { R, Student } from '../model/Student'export default function A3() {const [students, setStudents] = useState<Student[]>([])const [loading, setLoading] = useState(true)const [pagination, setPagination] = useState<TablePaginationConfig>({current:1, pageSize:5})// 参数: 新的分页数据function onTableChange(newPagination: TablePaginationConfig) {setPagination(newPagination)}useEffect(() => {async function getStudents() {const resp = await axios.get<R<Student[]>>('http://localhost:8080/api/students')setStudents(resp.data.data)setLoading(false)}getStudents()}, [])// ... 省略// pagination: 分页数据// onChange: 当页号,页大小改变时触发return (<Tablecolumns={columns}dataSource={students}rowKey='id'loading={loading}pagination={pagination}onChange={onTableChange}></Table>)
}
  • 本例还是查询所有数据,分页是客户端 Table 组件自己实现的
服务端分页
import { Table } from 'antd'
import { ColumnsType, TablePaginationConfig } from 'antd/lib/table'
import axios from 'axios'
import { useEffect, useState } from 'react'
import { PageResp, R, Student } from '../model/Student'export default function A4() {const [students, setStudents] = useState<Student[]>([])const [loading, setLoading] = useState(true)const [pagination, setPagination] = useState<TablePaginationConfig>({current: 1,pageSize: 5,})function onTableChange(newPagination: TablePaginationConfig) {setPagination(newPagination)}useEffect(() => {async function getStudents() {// params 用来给请求添加 url 后的 ? 参数const resp = await axios.get<R<PageResp<Student>>>('http://localhost:8080/api/students/q',{params: {page: pagination.current,size: pagination.pageSize,},})// 返回结果中:list 代表当前页集合, total 代表总记录数setStudents(resp.data.data.list)setPagination((old) => {return { ...old, total: resp.data.data.total }})setLoading(false)}getStudents()}, [pagination.current, pagination.pageSize])// useEffect 需要在依赖项( current 和 pageSize ) 改变时重新执行const columns: ColumnsType<Student> = [{title: '编号',dataIndex: 'id',},{title: '姓名',dataIndex: 'name',},{title: '性别',dataIndex: 'sex',},{title: '年龄',dataIndex: 'age',},]return (<Tablecolumns={columns}dataSource={students}rowKey='id'loading={loading}pagination={pagination}onChange={onTableChange}></Table>)
}
  • 本例需要服务端配合来实现分页,参见代码中新加的注释

其中 PageResp 类型定义为

export interface PageResp<T> {list: T[],total: number
}
条件查询
import { Input, Select, Table } from 'antd'
import { ColumnsType, TablePaginationConfig } from 'antd/lib/table'
import axios from 'axios'
import React, { useEffect, useState } from 'react'
import { PageResp, R, Student, StudentQueryForm } from '../model/Student'const { Option } = Selectexport default function A5() {const [students, setStudents] = useState<Student[]>([])const [loading, setLoading] = useState(true)const [pagination, setPagination] = useState<TablePaginationConfig>({current: 1,pageSize: 5,})// 代表查询条件的状态数据const [form, setForm] = useState<StudentQueryForm>({})function onTableChange(newPagination: TablePaginationConfig) {setPagination(newPagination)}useEffect(() => {async function getStudents() {const resp = await axios.get<R<PageResp<Student>>>('http://localhost:8080/api/students/q',{params: {page: pagination.current,size: pagination.pageSize,...form // 补充查询参数},})setStudents(resp.data.data.list)setPagination((old) => {return { ...old, total: resp.data.data.total }})setLoading(false)}getStudents()}, [pagination.current, pagination.pageSize, form.name, form.sex, form.age])// 依赖项除了分页条件外,新加了查询条件依赖const columns: ColumnsType<Student> = [{title: '编号',dataIndex: 'id',},{title: '姓名',dataIndex: 'name',},{title: '性别',dataIndex: 'sex',},{title: '年龄',dataIndex: 'age',},]// name 条件改变时处理函数function onNameChange(e: React.ChangeEvent<HTMLInputElement>) {setForm((old)=>{return {...old, name: e.target.value}})}// sex 条件改变时处理函数function onSexChange(value: string) {setForm((old)=>{return {...old, sex: value}})}// age 条件改变时处理函数function onAgeChange(value: string) {setForm((old)=>{return {...old, age: value}})}return (<div><div><Inputstyle={{ width: 120 }}placeholder='请输入姓名'value={form.name}onChange={onNameChange}></Input><Selectstyle={{ width: 120 }}placeholder='请选择性别'allowClear={true}value={form.sex}onChange={onSexChange}><Option value='男'>男</Option><Option value='女'>女</Option></Select><Selectstyle={{ width: 120 }}placeholder='请选择年龄'allowClear={true}value={form.age}onChange={onAgeChange}><Option value='1,19'>20以下</Option><Option value='20,29'>20左右</Option><Option value='30,39'>30左右</Option><Option value='40,120'>40以上</Option></Select></div><Tablecolumns={columns}dataSource={students}rowKey='id'loading={loading}pagination={pagination}onChange={onTableChange}></Table></div>)
}
  • 建议 axios 发请求是用 params 而不要自己拼字符串,因为自己拼串需要去掉值为 undefined 的属性

其中 StudentQueryForm 为

export interface StudentQueryForm {name?: string,sex?: string,age?: string,[key: string]: any
}
删除
import { Button, message, Popconfirm } from 'antd'
import axios from 'axios'
import { R } from '../model/Student'export default function A6Delete({ id, onSuccess }: { id: number, onSuccess:()=>void }) {async function onConfirm() {const resp = await axios.delete<R<string>>(`http://localhost:8080/api/students/${id}`)message.success(resp.data.data)// 改变 form 依赖项onSuccess()}return (<Popconfirm title='确定要删除学生吗?' onConfirm={onConfirm}><Button danger size='small'>删除</Button></Popconfirm>)
}

使用删除组件

import { Button, Input, Select, Space, Table } from 'antd'
import { ColumnsType, TablePaginationConfig } from 'antd/lib/table'
import axios from 'axios'
import React, { useEffect, useState } from 'react'
import { PageResp, R, Student, StudentQueryForm } from '../model/Student'
import A6Delete from './A6Delete'const { Option } = Selectexport default function A6() {// ... 省略function onDeleteSuccess() {setForm((old)=>{return {...old}})}const columns: ColumnsType<Student> = [// ... 省略{title: '操作',dataIndex: 'operation',// value: 属性值, studentrender: (_, student)=>{return <><Space><A6Delete id={student.id} onSuccess={onDeleteSuccess}></A6Delete><Button type='default' size='small'>修改</Button></Space></>}}]// ... 省略
}
修改
import { Form, Input, InputNumber, message, Modal, Radio } from 'antd'
import { Rule } from 'antd/lib/form'
import axios from 'axios'
import { useEffect } from 'react'
import { R, Student } from '../model/Student'export default function A6Update({open,student,onSuccess,onCancel,
}: {open: booleanstudent: StudentonSuccess?: () => voidonCancel?: () => void
}) {const { Item } = Formconst { Group } = Radioconst options = [{ label: '男', value: '男' },{ label: '女', value: '女' },]const [form] = Form.useForm() // 代表了表单对象const nameRules: Rule[] = [{ required: true, message: '姓名必须' },{ min: 2, type: 'string', message: '至少两个字符' },]const ageRules: Rule[] = [{ required: true, message: '年龄必须' },{ min: 1, type: 'number', message: '最小1岁' },{ max: 120, type: 'number', message: '最大120岁' },]async function onOk() {// 验证并获取表单数据try {const values = await form.validateFields()console.log(values)const resp = await axios.put<R<string>>(`http://localhost:8080/api/students/${values.id}`,values)message.success(resp.data.data)onSuccess && onSuccess()} catch (e) {console.error(e)}}useEffect(() => {// 修改表单数据form.setFieldsValue(student) // id, name, sex, age}, [student])return (<Modalopen={open}title='修改学生'onOk={onOk}onCancel={onCancel}forceRender={true}><Form form={form}><Item label='编号' name='id'><Input readOnly></Input></Item><Item label='姓名' name='name' rules={nameRules}><Input></Input></Item><Item label='性别' name='sex'><Groupoptions={options}optionType='button'buttonStyle='solid'></Group></Item><Item label='年龄' name='age' rules={ageRules}><InputNumber></InputNumber></Item></Form></Modal>)
}
  • forceRender 是避免因为使用 useForm 后,表单套在 Modal 中会出现的警告错误

使用组件

import { Button, Input, Select, Space, Table } from 'antd'
import { ColumnsType, TablePaginationConfig } from 'antd/lib/table'
import axios from 'axios'
import React, { useEffect, useState } from 'react'
import { PageResp, R, Student, StudentQueryForm } from '../model/Student'
import A6Delete from './A6Delete'
import A6Update from './A6Update'const { Option } = Selectexport default function A6() {// ... 省略const columns: ColumnsType<Student> = [// ... 省略{title: '操作',dataIndex: 'operation',// value: 属性值, studentrender: (_, student) => {return (<><Space><A6Delete id={student.id} onSuccess={onDeleteSuccess}></A6Delete><Buttontype='default'size='small'onClick={() => {onUpdateClick(student)}}>修改</Button></Space></>)},},]// -------------- 修改功能开始 -------------function onUpdateClick(student: Student) {setUpdateOpen(true)setUpdateForm(student)}function onUpdateCancel() {setUpdateOpen(false)}function onUpdateSuccess() {setUpdateOpen(false)setForm((old) => {return { ...old }})}const [updateOpen, setUpdateOpen] = useState(false)const [updateForm, setUpdateForm] = useState<Student>({id: 0,name: '',sex: '男',age: 18,})// -------------- 修改功能结束 -------------return (<div><A6Updateopen={updateOpen}student={updateForm}onSuccess={onUpdateSuccess}onCancel={onUpdateCancel}></A6Update><!-- ... 省略 --></div>)
}
新增
import { Form, Input, InputNumber, message, Modal, Radio } from 'antd'
import { Rule } from 'antd/lib/form'
import axios from 'axios'
import { useEffect } from 'react'
import { R, Student } from '../model/Student'export default function A6Insert({open,student,onSuccess,onCancel,
}: {open: booleanstudent: StudentonSuccess?: () => voidonCancel?: () => void
}) {const { Item } = Formconst { Group } = Radioconst options = [{ label: '男', value: '男' },{ label: '女', value: '女' },]const [form] = Form.useForm() // 代表了表单对象const nameRules: Rule[] = [{ required: true, message: '姓名必须' },{ min: 2, type: 'string', message: '至少两个字符' },]const ageRules: Rule[] = [{ required: true, message: '年龄必须' },{ min: 1, type: 'number', message: '最小1岁' },{ max: 120, type: 'number', message: '最大120岁' },]async function onOk() {// 验证并获取表单数据try {const values = await form.validateFields()console.log(values)const resp = await axios.post<R<string>>(`http://localhost:8080/api/students`,values)message.success(resp.data.data)onSuccess && onSuccess()form.resetFields() // 重置表单} catch (e) {console.error(e)}}return (<Modalopen={open}title='新增学生'onOk={onOk}onCancel={onCancel}forceRender={true}><Form form={form} initialValues={student}><Item label='姓名' name='name' rules={nameRules}><Input></Input></Item><Item label='性别' name='sex'><Groupoptions={options}optionType='button'buttonStyle='solid'></Group></Item><Item label='年龄' name='age' rules={ageRules}><InputNumber></InputNumber></Item></Form></Modal>)
}
  • initialValues 只会触发一次表单赋初值
  • form.resetFields() 会将表单重置为 initialValues 时的状态

使用组件

import { Button, Input, Select, Space, Table } from 'antd'
import { ColumnsType, TablePaginationConfig } from 'antd/lib/table'
import axios from 'axios'
import React, { useEffect, useState } from 'react'
import { PageResp, R, Student, StudentQueryForm } from '../model/Student'
import A6Delete from './A6Delete'
import A6Insert from './A6Insert'
import A6SelectedDelete from './A6SelectedDelete'
import A6Update from './A6Update'const { Option } = Selectexport default function A6() {// ... 省略// -------------- 新增功能开始 -------------function onInsertClick() {setInsertOpen(true)}function onInsertCancel() {setInsertOpen(false)}function onInsertSuccess() {setInsertOpen(false)setForm((old) => {return { ...old }})}const [insertOpen, setInsertOpen] = useState(false)const [insertForm, setInsertForm] = useState<Student>({id: 0,name: '',sex: '男',age: 18,})// -------------- 新增功能结束 -------------return (<div><A6Insertopen={insertOpen}student={insertForm}onSuccess={onInsertSuccess}onCancel={onInsertCancel}></A6Insert><A6Updateopen={updateOpen}student={updateForm}onSuccess={onUpdateSuccess}onCancel={onUpdateCancel}></A6Update><div><Space><Inputstyle={{ width: 120 }}placeholder='请输入姓名'value={form.name}onChange={onNameChange}></Input><Selectstyle={{ width: 120 }}placeholder='请选择性别'allowClear={true}value={form.sex}onChange={onSexChange}><Option value='男'>男</Option><Option value='女'>女</Option></Select><Selectstyle={{ width: 120 }}placeholder='请选择年龄'allowClear={true}value={form.age}onChange={onAgeChange}><Option value='1,19'>20以下</Option><Option value='20,29'>20左右</Option><Option value='30,39'>30左右</Option><Option value='40,120'>40以上</Option></Select><Button type='primary' onClick={onInsertClick}>新增</Button></Space></div><Tablecolumns={columns}dataSource={students}rowKey='id'loading={loading}pagination={pagination}onChange={onTableChange}></Table></div>)
}
删除选中
import { Button, message, Popconfirm } from "antd";
import axios from "axios";
import React from "react";
import { R } from "../model/Student";export default function A6DeleteSelected({ids, onSuccess}: {ids:React.Key[], onSuccess?:()=>void} // Key[] 是 number 或 string 的数组
){const disabled = ids.length === 0async function onConfirm() {const resp = await axios.delete<R<string>>('http://localhost:8080/api/students', {data: ids})message.success(resp.data.data)onSuccess && onSuccess()}return (<Popconfirm title='真的要删除选中的学生吗?' onConfirm={onConfirm} disabled={disabled}><Button danger type='primary' disabled={disabled}>删除选中</Button></Popconfirm>)
}

与 A6 结合

import { Button, Input, Select, Space, Table } from 'antd'
import { ColumnsType, TablePaginationConfig } from 'antd/lib/table'
import axios from 'axios'
import React, { useEffect, useState } from 'react'
import { PageResp, R, Student, StudentQueryForm } from '../model/Student'
import A6Delete from './A6Delete'
import A6Insert from './A6Insert'
import A6SelectedDelete from './A6SelectedDelete'
import A6Update from './A6Update'const { Option } = Selectexport default function A6() {// ... 省略// -------------- 删除选中功能开始 -------------const [ids, setIds] = useState<React.Key[]>([])function onIdsChange(ids:React.Key[]) {// console.log(ids)setIds(ids)}function onDeleteSelectedSuccess() {setForm((old)=>{return {...old}})setIds([])}// -------------- 删除选中功能结束 -------------return (<div><A6Insertopen={insertOpen}student={insertForm}onSuccess={onInsertSuccess}onCancel={onInsertCancel}></A6Insert><A6Updateopen={updateOpen}student={updateForm}onSuccess={onUpdateSuccess}onCancel={onUpdateCancel}></A6Update><div><Space><!-- ... 省略 --><A6SelectedDelete ids={ids} onSuccess={onDeleteSelectedSuccess}></A6SelectedDelete></Space></div><TablerowSelection={{selectedRowKeys: selectedKeys,onChange: onSelectChange,}}columns={columns}dataSource={students}rowKey='id'loading={loading}pagination={pagination}onChange={onTableChange}></Table></div>)
}
useRequest

安装

npm install ahooks

使用

import { useRequest } from 'ahooks'
import { Table } from 'antd'
import { ColumnsType } from 'antd/lib/table'
import axios from 'axios'
import { Student, R } from '../model/Student'export default function A3() {function getStudents() {return axios.get<R<Student[]>>('http://localhost:8080/api/students')}const { loading, data } = useRequest(getStudents)  const columns: ColumnsType<Student> = [{title: '编号',dataIndex: 'id',},{title: '姓名',dataIndex: 'name',},{title: '性别',dataIndex: 'sex',},{title: '年龄',dataIndex: 'age',},]return (<TabledataSource={data?.data.data}columns={columns}rowKey='id'loading={loading}pagination={{ hideOnSinglePage: true }}></Table>)
}
useAndtTable
import { useAntdTable } from 'ahooks'
import { Table } from 'antd'
import { ColumnsType } from 'antd/lib/table'
import axios from 'axios'
import { Student, R } from '../model/Student'interface PageResp<T> {total: numberlist: T[]
}interface PageReq {current: numberpageSize: numbersorter?: anyfilter?: any
}export default function A3() {async function getStudents({ current, pageSize }: PageReq) {const resp = await axios.get<R<PageResp<Student>>>(`http://localhost:8080/api/students/q?page=${current}&size=${pageSize}`)return resp.data.data}const { tableProps } = useAntdTable(getStudents, {defaultParams: [{ current: 1, pageSize: 5 }],})console.log(tableProps)const columns: ColumnsType<Student> = [{title: '编号',dataIndex: 'id',},{title: '姓名',dataIndex: 'name',},{title: '性别',dataIndex: 'sex',},{title: '年龄',dataIndex: 'age',},]return <Table {...tableProps} columns={columns} rowKey='id'></Table>
}

2) MobX

介绍

需求,组件0 改变了数据,其它组件也想获得改变后的数据,如图所示

在这里插入图片描述

这种多个组件之间要共享状态数据,useState 就不够用了,useContext 也不好用了

能够和 react 配合使用的状态管理库有

  • MobX
  • Redux

其中 Redux API 非常难以使用,这里选择了更加符合人类习惯的 MobX,它虽然采用了面向对象的语法,但也能和函数式的代码很好地结合

文档
  • MobX 中文文档
  • MobX 官方文档
安装
npm install mobx mobx-react-lite
  • mobx 目前版本是 6.x
  • mobx-react-lite 目前版本是 3.x
名词

在这里插入图片描述

  • Actions 用来修改状态数据的方法
  • Observable state 状态数据,可观察
  • Derived values 派生值,也叫 Computed values 计算值,会根据状态数据的改变而改变,具有缓存功能
  • Reactions 状态数据发生变化后要执行的操作,如 react 函数组件被重新渲染
使用

首先,定义一个在函数之外存储状态数据的 store,它与 useState 不同:

  • useState 里的状态数据是存储在每个组件节点上,不同组件之间没法共享
  • 而 MobX 的 store 就是一个普通 js 对象,只要保证多个组件都访问此对象即可
import axios from 'axios'
import { makeAutoObservable } from 'mobx'
import { R, Student } from '../model/Student'class StudentStore {student: Student = { name: '' }constructor() {makeAutoObservable(this)}async fetch(id: number) {const resp = await axios.get<R<Student>>(`http://localhost:8080/api/students/${id}`)runInAction(() => {this.student = resp.data.data})}get print() {const first = this.student.name.charAt(0)if (this.student.sex === '男') {return first.concat('大侠')} else if (this.student.sex === '女') {return first.concat('女侠')} else {return ''}} 
}export default new StudentStore()

其中 makeAutoObservable 会

  • 将对象的属性 student 变成 Observable state,即状态数据
  • 将对象的方法 fetch 变成 Action,即修改数据的方法
  • 将 get 方法变成 Computed values

在异步操作里为状态属性赋值,需要放在 runInAction 里,否则会有警告错误

使用 store,所有使用 store 的组件,为了感知状态数据的变化,需要用 observer 包装,对应着图中 reactions

import Search from 'antd/lib/input/Search'
import { observer } from 'mobx-react-lite'
import studentStore from '../store/StudentStore'
import A71 from './A71'
import Test2 from './Test2'const A7 = () => {return (<div><Searchplaceholder='input search text'onSearch={(v) => studentStore.fetch(Number(v))}style={{ width: 100 }}/><h3>组件0 {studentStore.student.name}</h3><A71></A71><A72></A72></div>)
}export default observer(A7)

其它组件

import { observer } from 'mobx-react-lite'
import studentStore from '../store/StudentStore'const A71 = () =>{return <h3 style={{color:'red'}}>组件1 {studentStore.student.name}</h3>
}export default observer(A71)
import { observer } from 'mobx-react-lite'
import studentStore from '../store/StudentStore'const A72 = () =>{return <h3 style={{color:'red'}}>组件1 {studentStore.student.name}</h3>
}export default observer(A72)
注解方式
import { R, Student } from "../model/Student";
import { action, computed, makeAutoObservable, makeObservable, observable, runInAction } from 'mobx'
import axios from "axios";class StudentStore {// 属性 - 对应状态数据 observable state@observable student: Student = { id: 0, name: '' }// 方法 - 对应 action 方法@action setName(name: string) {this.student.name = name}@action async fetch(id: number) {const resp = await axios.get<R<Student>>(`http://localhost:8080/api/students/${id}`)runInAction(() => {this.student = resp.data.data})}// get 方法 - 对应 derived value@computed get displayName() {const first = this.student.name.charAt(0)if (this.student.sex === '男') {return first + '大侠'} else if (this.student.sex === '女') {return first + '女侠'} else {return ''}}// 构造器constructor() {makeObservable(this)}
}export default new StudentStore()

需要在 tsconifg.json 中加入配置

{"compilerOptions": {// ..."experimentalDecorators": true}
}

3) React Router

安装
npm install react-router-dom
  • 目前版本是 6.x
使用

新建文件 src/router/router.tsx

import { lazy } from 'react'
import { Navigate, RouteObject, useRoutes } from 'react-router-dom'export function load(name: string) {const Page = lazy(() => import(`../pages/${name}`))return <Page></Page>
}const staticRoutes: RouteObject[] = [{ path: '/login', element: load('A8Login') },{path: '/',element: load('A8Main'),children: [{ path: 'student', element: load('A8MainStudent') },{ path: 'teacher', element: load('A8MainTeacher') },{ path: 'user', element: load('A8MainUser') }],},{ path: '/404', element: load('A8Notfound') },{ path: '/*', element: <Navigate to={'/404'}></Navigate> },
]export default function Router() {return useRoutes(staticRoutes)
}

index.tsx 修改为

import ReactDOM from 'react-dom/client';
import './index.css';
import { ConfigProvider } from 'antd';
import zhCN from 'antd/es/locale/zh_CN'import { BrowserRouter } from 'react-router-dom';
import Router from './router/router';const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement
);root.render(<ConfigProvider locale={zhCN}><BrowserRouter><Router></Router></BrowserRouter></ConfigProvider>  
)

A8Main 的代码

import { Layout } from "antd";
import { Link, Outlet } from "react-router-dom";export default function A8Main () {  return <Layout><Layout.Header>头部导航</Layout.Header><Layout><Layout.Sider>侧边导航<Link to='/student'>学生管理</Link><Link to='/teacher'>教师管理</Link><Link to='/user'>用户管理</Link></Layout.Sider><Layout.Content><Outlet></Outlet></Layout.Content></Layout></Layout>
}
  1. Navigate 的作用是重定向
  2. load 方法的作用是懒加载组件,更重要的是根据字符串找到真正的组件,这是动态路由所需要的
  3. children 来进行嵌套路由映射,嵌套路由在跳转后,并不是替换整个页面,而是用新页面替换父页面的 Outlet 部分
动态路由

路由分成两部分:

  • 静态路由,固定的部分,如主页、404、login 这几个页面
  • 动态路由,变化的部分,经常是主页内的嵌套路由,比如 Student、Teacher 这些

动态路由应该是根据用户登录后,根据角色的不同,从后端服务获取,因为这些数据是变化的,所以用 mobx 来管理

import axios from 'axios'
import { makeAutoObservable, runInAction } from 'mobx'
import { Navigate, RouteObject } from 'react-router-dom'
import { MenuAndRoute, R, Route } from '../model/Student'
import { load } from '../router/MyRouter'class RoutesStore {dynamicRoutes: Route[]async fetch(username: string) {const resp = await axios.get<R<MenuAndRoute>>(`http://localhost:8080/api/menu/${username}`)runInAction(() => {this.dynamicRoutes = resp.data.data.routeListlocalStorage.setItem('dynamicRoutes', JSON.stringify(this.dynamicRoutes))})}constructor() {makeAutoObservable(this)const r = localStorage.getItem('dynamicRoutes')this.dynamicRoutes = r ? JSON.parse(r) : []}reset() {this.dynamicRoutes = []localStorage.removeItem('dynamicRoutes')}get routes() {const staticRoutes: RouteObject[] = [{ path: '/login', element: load('A8Login') },{ path: '/', element: load('A8Main') },{ path: '/404', element: load('A8Notfound') },{ path: '/*', element: <Navigate to={'/404'}></Navigate> },]const main = staticRoutes[1]main.children = this.dynamicRoutes.map((r) => {console.log(r.path, r.element)return {path: r.path,element: load(r.element),}})return staticRoutes}
}export default new RoutesStore()
  • 其中用 localStorage 进行了数据的持久化,避免刷新后丢失数据

MyRouter 文件修改为

import { observer } from 'mobx-react-lite'
import { lazy } from 'react'
import { Navigate, RouteObject, useRoutes } from 'react-router-dom'
import RoutesStore from '../store/RoutesStore'// 把字符串组件 => 组件标签
export function load(name: string) {// A8Loginconst Page = lazy(() => import(`../pages/${name}`))return <Page></Page>
}// 路由对象
function MyRouter() {  const router = useRoutes(RoutesStore.routes)return router
}export default observer(MyRouter)

注意导入 router 对象时,用 observer 做了包装,这样能够在 store 发生变化时重建 router 对象

动态菜单

图标要独立安装依赖

npm install @ant-design/icons

图标组件,用来将字符串图标转换为标签图标

import * as icons from '@ant-design/icons'interface Module {[p: string]: any
}const all: Module = iconsexport default function Icon({ name }: { name: string }) {const Icon = all[name]return <Icon></Icon>
}

修改 RoutesStore.tsx

import axios from 'axios'
import { makeAutoObservable, runInAction } from 'mobx'
import { Link, Navigate, RouteObject } from 'react-router-dom'
import { Menu, MenuAndRoute, R, Route } from '../model/Student'
import { load } from '../router/MyRouter'
import Icon from './Icon'function convertMenu(m: Menu): any {const Label = m.routePath ? <Link to={m.routePath}>{m.label}</Link> : m.labelreturn {label: Label,key: m.key,icon: <Icon name={m.icon}></Icon>,children: m.children && m.children.map(convertMenu)}
}class RoutesStore {// 动态部分dynamicRoutes: Route[] = []dynamicMenus: Menu[] = []async fetch(username: string) {const resp = await axios.get<R<MenuAndRoute>>(`http://localhost:8080/api/menu/${username}`)runInAction(() => {this.dynamicRoutes = resp.data.data.routeListlocalStorage.setItem('dynamicRoutes', JSON.stringify(this.dynamicRoutes))this.dynamicMenus = resp.data.data.menuTreelocalStorage.setItem('dynamicMenus', JSON.stringify(this.dynamicMenus))})}get menus() {return this.dynamicMenus.map(convertMenu)}get routes() {const staticRoutes: RouteObject[] = [{ path: '/login', element: load('A8Login') },{ path: '/', element: load('A8Main'), children: [] },{ path: '/404', element: load('A8Notfound') },{ path: '/*', element: <Navigate to={'/404'}></Navigate> },]staticRoutes[1].children = this.dynamicRoutes.map((r) => {return {path: r.path,element: load(r.element),}})return staticRoutes}constructor() {makeAutoObservable(this)const json = localStorage.getItem('dynamicRoutes')this.dynamicRoutes = json ? JSON.parse(json) : []const json2 = localStorage.getItem('dynamicMenus')this.dynamicMenus = json2 ? JSON.parse(json2) : []}reset() {localStorage.removeItem('dynamicRoutes')this.dynamicRoutes = []localStorage.removeItem('dynamicMenus')this.dynamicMenus = []}
}export default new RoutesStore()

其中 convertMenu 为核心方法,负责将服务器返回的 Menu 转换成 antd Menu 组件需要的 Menu

使用

<Menu items={RoutesStore.menus} mode='inline' theme="dark"></Menu>

跳转若发生错误,可能是因为组件懒加载引起的,需要用 Suspense 解决

root.render(<ConfigProvider locale={zhCN}><BrowserRouter><Suspense fallback={<h3>加载中...</h3>}><MyRouter></MyRouter></Suspense></BrowserRouter></ConfigProvider>
)
登录
import { ItemType } from 'antd/lib/menu/hooks/useItems'
import axios from 'axios'
import { makeAutoObservable, runInAction } from 'mobx'
import { Link, Navigate, RouteObject } from 'react-router-dom'
import { LoginReq, LoginResp, Menu, MenuAndRoute, R, Route } from '../model/Student'
import { load } from '../router/MyRouter'
import Icon from './Icon'function convertMenu(m: Menu): ItemType {const Label = m.routePath? <Link to={m.routePath}>{m.label}</Link> : m.labelreturn {key: m.key,label: Label,icon: <Icon name={m.icon}></Icon>, children: m.children && m.children.map(convertMenu)}
}class RoutesStore {// 动态部分dynamicRoutes: Route[] = []dynamicMenus: Menu[] = []token: string = ''state: string = 'pending' // 取值 pending done errormessage: string = '' // 取值: 1. 空串 正常  2. 非空串 错误消息async login(loginReq: LoginReq) {this.state = 'pending'this.message = ''const resp1 = await axios.post<R<LoginResp>>('http://localhost:8080/api/loginJwt',loginReq)if(resp1.data.code === 999) {const resp2 = await axios.get<R<MenuAndRoute>>(`http://localhost:8080/api/menu/${loginReq.username}`)runInAction(()=>{this.token = resp1.data.data.tokenlocalStorage.setItem('token', this.token)this.dynamicRoutes = resp2.data.data.routeListlocalStorage.setItem('dynamicRoutes', JSON.stringify(this.dynamicRoutes))this.dynamicMenus = resp2.data.data.menuTreelocalStorage.setItem('dynamicMenus', JSON.stringify(this.dynamicMenus))this.state = 'done'})} else {runInAction(()=>{this.message = resp1.data.message || '未知错误'this.state = 'error'})}}async fetch(username: string) {const resp = await axios.get<R<MenuAndRoute>>(`http://localhost:8080/api/menu/${username}`)runInAction(() => {this.dynamicRoutes = resp.data.data.routeListlocalStorage.setItem('dynamicRoutes', JSON.stringify(this.dynamicRoutes))this.dynamicMenus = resp.data.data.menuTreelocalStorage.setItem('dynamicMenus', JSON.stringify(this.dynamicMenus))})}get routes() {const staticRoutes: RouteObject[] = [{ path: '/login', element: load('A8Login') },{ path: '/', element: load('A8Main'), children: [] },{ path: '/404', element: load('A8Notfound') },{ path: '/*', element: <Navigate to={'/404'}></Navigate> },]staticRoutes[1].children = this.dynamicRoutes.map((r) => {return {path: r.path,element: load(r.element),}})return staticRoutes}get menus() {return this.dynamicMenus.map(convertMenu)}constructor() {makeAutoObservable(this)const json = localStorage.getItem('dynamicRoutes')this.dynamicRoutes = json ? JSON.parse(json) : []const json1 = localStorage.getItem('dynamicMenus')this.dynamicMenus = json1 ? JSON.parse(json1) : []const token = localStorage.getItem('token')this.token = token ?? ''this.message = ''this.state = 'pending'  }reset() {localStorage.removeItem('dynamicRoutes')this.dynamicRoutes = []localStorage.removeItem('dynamicMenus')this.dynamicMenus = []localStorage.removeItem('token')this.token = ''this.message = ''this.state = 'pending'    }
}export default new RoutesStore()

登录页面

function A8Login() {function onFinish(values: { username: string; password: string }) {RoutesStore.login(values)}const nav = useNavigate()useEffect(() => {if (RoutesStore.state === 'done') {nav('/')} else if (RoutesStore.state === 'error') {message.error(RoutesStore.message)}}, [RoutesStore.state])// ...
}export default observer(A8Login)
  • 用 useNavigate() 返回的函数跳转的代码不能包含在函数式组件的主逻辑中,只能放在
    • 其它事件处理函数中
    • 写在副作用函数 useEffect 之中
注销、欢迎词、登录检查

Store 中增加 get username 方法

class RoutesStore {// ...// eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhZG1pbiJ9.-l-MjMPGJVOf3zoIJgoqpV3LWoqvCCgcaI1ga86ismUget username() {if(this.token.length === 0) {return ''}const json = atob(this.token.split('.')[1])return JSON.parse(json).sub}// ...
}
  • token 的前两部分都可以解码出来,其中 [1] 就是 token 的内容部分

主页组件改为

import { Button, Layout, Menu } from 'antd'
import { observer } from 'mobx-react-lite'
import { useEffect } from 'react'
import { Navigate, Outlet, useNavigate } from 'react-router-dom'
import RoutesStore from '../store/RoutesStore'function A8Main() {const nav = useNavigate()function onClick() {RoutesStore.reset()nav('/login')}/* useEffect(()=>{if(RoutesStore.username === '') {nav('/login')}}, []) */if(RoutesStore.username === '') {return <Navigate to='/login'></Navigate>}return (<Layout><Layout.Header><span>欢迎您【{RoutesStore.username}】</span><Button size='small' onClick={onClick}>注销</Button></Layout.Header><Layout><Layout.Sider><Menu items={RoutesStore.menus} theme='dark' mode='inline'></Menu></Layout.Sider><Layout.Content><Outlet></Outlet></Layout.Content></Layout></Layout>)
}export default observer(A8Main)
  • 这个例子中推荐用 Navigate 来完成跳转
  • /student,/teacher 等路由不需要检查,因为登录成功后才有

附录

代码片段

ctrl+shift+p 输入关键词代码

在这里插入图片描述

定义 fun.code-snippets

{"函数组件": {"scope": "javascript,typescript,typescriptreact","prefix": "fun","body": ["export default function ${1:函数名} () {","  $0",      "  return <></>","}"],"description": "快速生成react函数式组件"}
}

定义 ofun.code-snippets

{"mobx函数组件": {"scope": "javascript,typescript,typescriptreact","prefix": "ofun","body": ["import { observer } from \"mobx-react-lite\"","","function ${1:函数名} () {","  $0",      "  return <></>","}","export default observer($1)",],"description": "快速生成mobx react函数式组件"}
}

这样可以在 tsx 中用快捷键 fun 以及 ofun 创建相应的代码片段

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/11266.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

[ESP32:Vscode+PlatformIO]新建工程 常用配置与设置

2025-1-29 一、新建工程 选择一个要创建工程文件夹的地方&#xff0c;在空白处鼠标右键选择通过Code打开 打开Vscode&#xff0c;点击platformIO图标&#xff0c;选择PIO Home下的open&#xff0c;最后点击new project 按照下图进行设置 第一个是工程文件夹的名称 第二个是…

3、从langchain到rag

文章目录 本文介绍向量和向量数据库向量向量数据库 索引开始动手实现rag加载文档数据并建立索引将向量存放到向量数据库中检索生成构成一条链 本文介绍 从本节开始&#xff0c;有了上一节的langchain基础学习&#xff0c;接下来使用langchain实现一个rag应用&#xff0c;并稍微…

【自然语言处理(NLP)】基于Transformer架构的预训练语言模型:BERT 训练之数据集处理、训练代码实现

文章目录 介绍BERT 训练之数据集处理BERT 原理及模型代码实现数据集处理导包加载数据生成下一句预测任务的数据从段落中获取nsp数据生成遮蔽语言模型任务的数据从token中获取mlm数据将文本转换为预训练数据集创建Dataset加载WikiText-2数据集 BERT 训练代码实现导包加载数据构建…

41【文件名的编码规则】

我们在学习的过程中&#xff0c;写出数据或读取数据时需要考虑编码类型 火山采用&#xff1a;UTF-16 易语言采用&#xff1a;GBK php采用&#xff1a;UTF-8 那么我们写出的文件名应该是何种编码的&#xff1f;比如火山程序向本地写出一个“测试.txt”&#xff0c;理论上这个“测…

NLP深度学习 DAY4:Word2Vec详解:两种模式(CBOW与Skip-gram)

用稀疏向量表示文本&#xff0c;即所谓的词袋模型在 NLP 有着悠久的历史。正如上文中介绍的&#xff0c;早在 2001年就开始使用密集向量表示词或词嵌入。Mikolov等人在2013年提出的创新技术是通过去除隐藏层&#xff0c;逼近目标&#xff0c;进而使这些单词嵌入的训练更加高效。…

HarmonyOS简介:应用开发的机遇、挑战和趋势

问题 更多的智能设备并没有带来更好的全场景体验 连接步骤复杂数据难以互通生态无法共享能力难以协同 主要挑战 针对不同设备上的不同操作系统&#xff0c;重复开发&#xff0c;维护多套版本 多种语言栈&#xff0c;对人员技能要求高 多种开发框架&#xff0c;不同的编程…

Windows11 不依赖docker搭建 deepseek-R1 1.5B版本(附 Open WebUi搭建方式)

零、前言 过年这几天发现 DeepSeek 非常火&#xff0c;试用了一下发现确实不错。与豆包、kimi、perplexity 这些相比完全不是一个次元的存在&#xff0c;特别是用ta写文章的时候体验非常好。所以试着自己搭一个环境。 一、安装 Ollama和DeepSeek-R1 我的安装方式很简单&#xf…

解决whisper 本地运行时GPU 利用率不高的问题

我在windows 环境下本地运行whisper 模型&#xff0c;使用的是nivdia RTX4070 显卡&#xff0c;结果发现GPU 的利用率只有2% 。使用 import torch print(torch.cuda.is_available()) 返回TRUE。表示我的cuda 是可用的。 最后在github 的下列网页上找到了问题 极低的 GPU 利…

springCload快速入门

原作者&#xff1a;3. SpringCloud - 快速通关 前置知识&#xff1a; Java17及以上、MavenSpringBoot、SpringMVC、MyBatisLinux、Docker 1. 分布式基础 1.1. 微服务 微服务架构风格&#xff0c;就像是把一个单独的应用程序开发为一套小服务&#xff0c;每个小服务运行在自…

Gradle配置指南:深入解析settings.gradle.kts(Kotlin DSL版)

文章目录 Gradle配置指南&#xff1a;深入解析settings.gradle.kts&#xff08;Kotlin DSL版&#xff09;settings.gradle.kts 基础配置选项单项目配置多项目配置 高级配置选项插件管理&#xff08;Plugin Management&#xff09;基础配置模板案例&#xff1a;Android项目标准配…

C++ Primer 标准库类型string

欢迎阅读我的 【CPrimer】专栏 专栏简介&#xff1a;本专栏主要面向C初学者&#xff0c;解释C的一些基本概念和基础语言特性&#xff0c;涉及C标准库的用法&#xff0c;面向对象特性&#xff0c;泛型特性高级用法。通过使用标准库中定义的抽象设施&#xff0c;使你更加适应高级…

[EAI-028] Diffusion-VLA,能够进行多模态推理和机器人动作预测的VLA模型

Paper Card 论文标题&#xff1a;Diffusion-VLA: Scaling Robot Foundation Models via Unified Diffusion and Autoregression 论文作者&#xff1a;Junjie Wen, Minjie Zhu, Yichen Zhu, Zhibin Tang, Jinming Li, Zhongyi Zhou, Chengmeng Li, Xiaoyu Liu, Yaxin Peng, Chao…

使用MATLAB进行雷达数据采集可视化

本文使用轮趣科技N10雷达&#xff0c;需要源码可在后台私信或者资源自取 1. 项目概述 本项目旨在通过 MATLAB 读取 N10 激光雷达 的数据&#xff0c;并进行 实时 3D 点云可视化。数据通过 串口 传输&#xff0c;并经过解析后转换为 三维坐标点&#xff0c;最终使用 pcplayer 进…

UE求职Demo开发日志#19 给物品找图标,实现装备增加属性,背包栏UI显示装备

1 将用到的图标找好&#xff0c;放一起 DataTable里对应好图标 测试一下能正确获取&#xff1a; 2 装备增强属性思路 给FMyItemInfo添加一个枚举变量记录类型&#xff08;物品&#xff0c;道具&#xff0c;装备&#xff0c;饰品&#xff0c;武器&#xff09;--> 扩展DataT…

Docker 部署 Starrocks 教程

Docker 部署 Starrocks 教程 StarRocks 是一款高性能的分布式分析型数据库&#xff0c;主要用于 OLAP&#xff08;在线分析处理&#xff09;场景。它最初是由百度的开源团队开发的&#xff0c;旨在为大数据分析提供一个高效、低延迟的解决方案。StarRocks 支持实时数据分析&am…

(9) 上:学习与验证 linux 里的 epoll 对象里的 EPOLLIN、 EPOLLHUP 与 EPOLLRDHUP 的不同

&#xff08;1&#xff09;经过之前的学习。俺认为结论是这样的&#xff0c;因为三次握手到四次挥手&#xff0c;到 RST 报文&#xff0c;都是 tcp 连接上收到了报文&#xff0c;这都属于读事件。所以&#xff1a; EPOLLIN : 包含了读事件&#xff0c; FIN 报文的正常四次挥手、…

python学opencv|读取图像(五十二)使用cv.matchTemplate()函数实现最佳图像匹配

【1】引言 前序学习了图像的常规读取和基本按位操作技巧&#xff0c;相关文章包括且不限于&#xff1a; python学opencv|读取图像-CSDN博客 python学opencv|读取图像&#xff08;四十九&#xff09;原理探究&#xff1a;使用cv2.bitwise()系列函数实现图像按位运算-CSDN博客…

数据分析系列--⑦RapidMiner模型评价(基于泰坦尼克号案例含数据集)

一、前提 二、模型评估 1.改造⑥ 2.Cross Validation算子说明 2.1Cross Validation 的作用 2.1.1 模型评估 2.1.2 减少过拟合 2.1.3 数据利用 2.2 Cross Validation 的工作原理 2.2.1 数据分割 2.2.2 迭代训练与测试 ​​​​​​​ 2.2.3 结果汇总 ​​​​​​​ …

DeepSeek r1本地安装全指南

环境基本要求 硬件配置 需要本地跑模型&#xff0c;兼顾质量、性能、速度以及满足日常开发需要&#xff0c;我们需要准备以下硬件&#xff1a; CPU&#xff1a;I9内存&#xff1a;128GB硬盘&#xff1a;3-4TB 最新SSD&#xff0c;C盘确保有400GB&#xff0c;其它都可划成D盘…

AI开发学习之——PyTorch框架

PyTorch 简介 PyTorch &#xff08;Python torch&#xff09;是由 Facebook AI 研究团队开发的开源机器学习库&#xff0c;广泛应用于深度学习研究和生产。它以动态计算图和易用性著称&#xff0c;支持 GPU 加速计算&#xff0c;并提供丰富的工具和模块。 PyTorch的主要特点 …