目录
(3)、使用React Toolkit 创建 counterStore
二十二、React-TypeScrip项目注意事项(Vite)
一、搭建(CRA)
npx create-react-app react-basic
//npx create-react-app 项目名称
-
npx - Node.js工具命令,查找并执行后续的包命令
-
create-react-app - 核心包(固定写法),用于创建React项目
-
react-basic React项目的名称(可以自定义) :::warning 创建React项目的更多方式 启动一个新的 React 项目 – React 中文文档 :::
运行
npm start
二、jsx
1、js表达式
在JSX中可以通过
大括号语法{}
识别JavaScript中的表达式,比如常见的变量、函数调用、方法调用等等
-
使用引号传递字符串
-
使用JS变量
-
函数调用和方法调用
-
使用JavaScript对象 :::warning 注意:if语句、switch语句、变量声明不属于表达式,不能出现在{}中 :::
2、列表渲染
const list = [
{id:1001, name:'Vue'},
{id:1002, name: 'React'},
{id:1003, name: 'Angular'}
]
function App(){
return (
<ul>
{list.map(item=><li key={item.id}>{item}</li>)}
</ul>
)
}
3、条件渲染
简单
const flag = true
const loading = false
function App(){
return (
<>
{flag && <span>this is span</span>}
{loading ? <span>loading...</span>:<span>this is span</span>}
</>
)
}
复杂
const type = 1 // 0|1|3
function getArticleJSX(){
if(type === 0){
return <div>无图模式模版</div>
}else if(type === 1){
return <div>单图模式模版</div>
}else(type === 3){
return <div>三图模式模版</div>
}
}
function App(){
return (
<>
{ getArticleJSX() }
</>
)
}
三、事件绑定
四、基础组件
在React中,一个组件就是首字母大写的函数,内部存放了组件的逻辑和视图UI, 渲染组件只需要把组件当成标签书写即可
五、组件状态管理
1、基本使用
2、状态修改规则
3、修改对象状态
4、组件的基础样式处理
5、lodash
npm i lodash
import _ from 'lodash'
// orderBy(对谁进行排序, 按照谁来排, 顺序)
newList = _.orderBy(list, 'ctime', 'desc')
6、classNames(动态管理css类)
npm i classnames
import classNames from 'classnames'
7、uuid(生成uuid)
npm i uuid
import { v4 as uuidV4 } form 'uuid'
调用
uuidV4() 创建随机uuid
8、dayjs(格式化时间)
npm i dayjs
import dayjs from 'dayjs'
六、React表单控制
1、受控绑定
2、非受控绑定
七、组件通信
1、父传子
实现步骤
-
父组件传递数据 - 在子组件标签上绑定属性
-
子组件接收数据 - 子组件通过props参数接收数据
function Son(props){
return <div>{ props.name }</div>
}
function App(){
const name = 'this is app name'
return (
<div>
<Son name={name}/>
</div>
)
}
2、子传父
核心思路:在子组件中调用父组件中的函数并传递参数
function Son({ onGetMsg }){
const sonMsg = 'this is son msg'
return (
<div>
{/* 在子组件中执行父组件传递过来的函数 */}
<button onClick={()=>onGetMsg(sonMsg)}>send</button>
</div>
)
}
function App(){
const getMsg = (msg)=>console.log(msg)
return (
<div>
{/* 传递父组件中的函数到子组件 */}
<Son onGetMsg={ getMsg }/>
</div>
)
}
3、兄弟通信
// 1. 通过子传父 A -> App
// 2. 通过父传子 App -> B
import { useState } from "react"
function A ({ onGetAName }) {
// Son组件中的数据
const name = 'this is A name'
return (
<div>
this is A compnent,
<button onClick={() => onGetAName(name)}>send</button>
</div>
)
}
function B ({ name }) {
return (
<div>
this is B compnent,
{name}
</div>
)
}
function App () {
const [name, setName] = useState('')
const getAName = (name) => {
setName(name)
}
return (
<div>
this is App
<A onGetAName={getAName} />
<B name={name} />
</div>
)
}
export default App
4、跨层通信
实现步骤:
-
使用
createContext
方法创建一个上下文对象Ctx -
在顶层组件(App)中通过
Ctx.Provider
组件提供数据 -
在底层组件(B)中通过
useContext
钩子函数获取消费数据
// App -> A -> B
import { createContext, useContext } from "react"
// 1. createContext方法创建一个上下文对象
const MsgContext = createContext()
function A () {
return (
<div>
this is A component
<B />
</div>
)
}
function B () {
// 3. 在底层组件 通过useContext钩子函数使用数据
const msg = useContext(MsgContext)
return (
<div>
this is B compnent,{msg}
</div>
)
}
function App () {
const msg = 'this is app msg'
return (
<div>
{/* 2. 在顶层组件 通过Provider组件提供数据 */}
<MsgContext.Provider value={msg}>
this is App
<A />
</MsgContext.Provider>
</div>
)
}
export default App
八、React副作用管理-useEffect
1、概念理解
useEffect是一个React Hook函数,用于在React组件中创建不是由事件引起而是由渲染本身引起的操作(副作用), 比 如发送AJAX请求,更改DOM等等
2、基础使用
3、useEffect依赖说明
useEffect副作用函数的执行时机存在多种情况,根据传入依赖项的不同,会有不同的执行表现
依赖项 | 副作用功函数的执行时机 |
---|---|
没有依赖项 | 组件初始渲染 + 组件更新时执行 |
空数组依赖 | 只在初始渲染时执行一次 |
添加特定依赖项 | 组件初始渲染 + 依赖项变化时执行 |
4、清除副作用
概念:在useEffect中编写的由渲染本身引起的对接组件外部的操作,社区也经常把它叫做副作用操作,比如在useEffect中开启了一个定时器,我们想在组件卸载时把这个定时器再清理掉,这个过程就是清理副作用
import { useEffect, useState } from "react"
function Son () {
// 1. 渲染时开启一个定时器
useEffect(() => {
const timer = setInterval(() => {
console.log('定时器执行中...')
}, 1000)
return () => {
// 清除副作用(组件卸载时)
clearInterval(timer)
}
}, [])
return <div>this is son</div>
}
function App () {
// 通过条件渲染模拟组件卸载
const [show, setShow] = useState(true)
return (
<div>
{show && <Son />}
<button onClick={() => setShow(false)}>卸载Son组件</button>
</div>
)
}
export default App
九、自定义Hook实现
1、使用
// 封装自定义Hook
// 问题: 布尔切换的逻辑 当前组件耦合在一起的 不方便复用
// 解决思路: 自定义hook
import { useState } from "react"
function useToggle () {
// 可复用的逻辑代码
const [value, setValue] = useState(true)
const toggle = () => setValue(!value)
// 哪些状态和回调函数需要在其他组件中使用 return
return {
value,
toggle
}
}
// 封装自定义hook通用思路
// 1. 声明一个以use打头的函数
// 2. 在函数体内封装可复用的逻辑(只要是可复用的逻辑)
// 3. 把组件中用到的状态或者回调return出去(以对象或者数组)
// 4. 在哪个组件中要用到这个逻辑,就执行这个函数,解构出来状态和回调进行使用
function App () {
const { value, toggle } = useToggle()
return (
<div>
{value && <div>this is div</div>}
<button onClick={toggle}>toggle</button>
</div>
)
}
export default App
2、规则
十、Redux
1、快速使用
使用步骤:
-
定义一个 reducer 函数 (根据当前想要做的修改返回一个新的状态)
-
使用createStore方法传入 reducer函数 生成一个store实例对象
-
使用store实例的 subscribe方法 订阅数据的变化(数据一旦变化,可以得到通知)
-
使用store实例的 dispatch方法提交action对象 触发数据变化(告诉reducer你想怎么改数据)
-
使用store实例的 getState方法 获取最新的状态数据更新到视图中
<button id="decrement">-</button>
<span id="count">0</span>
<button id="increment">+</button>
<script src="https://unpkg.com/redux@latest/dist/redux.min.js"></script>
<script>
// 定义reducer函数
// 内部主要的工作是根据不同的action 返回不同的state
function counterReducer (state = { count: 0 }, action) {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 }
case 'DECREMENT':
return { count: state.count - 1 }
default:
return state
}
}
// 使用reducer函数生成store实例
const store = Redux.createStore(counterReducer)
// 订阅数据变化
store.subscribe(() => {
console.log(store.getState())
document.getElementById('count').innerText = store.getState().count
})
// 增
const inBtn = document.getElementById('increment')
inBtn.addEventListener('click', () => {
store.dispatch({
type: 'INCREMENT'
})
})
// 减
const dBtn = document.getElementById('decrement')
dBtn.addEventListener('click', () => {
store.dispatch({
type: 'DECREMENT'
})
})
</script>
2、React集成Redux
在React中使用redux,官方要求安装俩个其他插件 - Redux Toolkit 和 react-redux
-
Redux Toolkit(RTK)- 官方推荐编写Redux逻辑的方式,是一套工具的集合集,简化书写方式
-
react-redux - 用来 链接 Redux 和 React组件 的中间件
(1)、环境配置
1.使用 CRA 快速创建 React 项目
npx create-react-app 项目名称
2.安装配套工具
npm i @reduxjs/toolkit react-redux
3.启动项目
npm run start
(2)、store目录结构设计
-
通常集中状态管理的部分都会单独创建一个单独的
store
目录 -
应用通常会有很多个子store模块,所以创建一个
modules
目录,在内部编写业务分类的子store -
store中的入口文件 index.js 的作用是组合modules中所有的子模块,并导出store
(3)、使用React Toolkit 创建 counterStore
counterStore.js(子模块创建)
import { createSlice } from '@reduxjs/toolkit'
const counterStore = createSlice({
// 模块名称独一无二
name: 'counter',
// 初始数据
initialState: {
count: 1
},
// 修改数据的同步方法
reducers: {
increment (state) {
state.count++
},
decrement(state){
state.count--
}
}
})
// 结构出actionCreater
const { increment,decrement } = counter.actions
// 获取reducer函数
const counterReducer = counterStore.reducer
// 导出
export { increment, decrement }
export default counterReducer
store/index.js (子模块组合)
import { configureStore } from '@reduxjs/toolkit'
import counterReducer from './modules/counterStore'
export default configureStore({
reducer: {
// 注册子模块
counter: counterReducer
}
})
(4)、为React注入store
react-redux负责把Redux和React 链接 起来,内置 Provider组件 通过 store 参数把创建好的store实例注入到应用中,链接正式建立
index.js
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
// 导入store
import store from './store'
// 导入store提供组件Provider
import { Provider } from 'react-redux'
ReactDOM.createRoot(document.getElementById('root')).render(
// 提供store数据
<Provider store={store}>
<App />
</Provider>
)
(5)、React组件使用store中的数据
在React组件中使用store中的数据,需要用到一个钩子函数 - useSelector,它的作用是把store中的数据映射到组件中,使用样例如下
(6)、React组件修改store中的数据
React组件中修改store中的数据需要借助另外一个hook函数 - useDispatch,它的作用是生成提交action对象的dispatch函数,使用样例如下:
3、Redux与React - 提交action传参
需求:组件中有俩个按钮
add to 10
和add to 20
可以直接把count值修改到对应的数字,目标count值是在组件中传递过去的,需要在提交action的时候传递参数
实现方式:在reducers的同步修改方法中添加action对象参数,在调用actionCreater的时候传递参数,参数会被传递到action对象payload属性上
4、Redux与React - 异步action处理
需求理解
实现步骤
-
创建store的写法保持不变,配置好同步修改状态的方法
-
单独封装一个函数,在函数内部return一个新函数,在新函数中 2.1 封装异步请求获取数据 2.2 调用同步actionCreater传入异步数据生成一个action对象,并使用dispatch提交
-
组件中dispatch的写法保持不变
代码实现
import { createSlice } from '@reduxjs/toolkit'
import axios from 'axios'
const channelStore = createSlice({
name: 'channel',
initialState: {
channelList: []
},
reducers: {
setChannelList (state, action) {
state.channelList = action.payload
}
}
})
// 创建异步
const { setChannelList } = channelStore.actions
const url = 'http://geek.itheima.net/v1_0/channels'
// 封装一个函数 在函数中return一个新函数 在新函数中封装异步
// 得到数据之后通过dispatch函数 触发修改
const fetchChannelList = () => {
return async (dispatch) => {
const res = await axios.get(url)
dispatch(setChannelList(res.data.data.channels))
}
}
export { fetchChannelList }
const channelReducer = channelStore.reducer
export default channelReducer
import { useEffect } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { fetchChannelList } from './store/channelStore'
function App () {
// 使用数据
const { channelList } = useSelector(state => state.channel)
useEffect(() => {
dispatch(fetchChannelList())
}, [dispatch])
return (
<div className="App">
<ul>
{channelList.map(task => <li key={task.id}>{task.name}</li>)}
</ul>
</div>
)
}
export default App
十一、ReactRouter 路由
1、简单上手
# 使用CRA创建项目
npm create-react-app 项目名称
# 安装最新的ReactRouter包
npm i react-router-dom
# 启动项目
npm run start
入口文件index.js
import React from 'react'
import ReactDOM from 'react-dom/client'
const router = createBrowserRouter([
{
path:'/login',
element: <div>登录</div>
},
{
path:'/article',
element: <div>文章</div>
}
])
ReactDOM.createRoot(document.getElementById('root')).render(
<RouterProvider router={router}/>
)
2、抽象路由模块
目录结构
article/index.js
router/index.js
入口文件index.js
3、路由导航
(1)、声明式导航
声明式导航是指通过在模版中通过 <Link/>
组件描述出要跳转到哪里去,比如后台管理系统的左侧菜单通常使用这种方式进行
语法说明:通过给组件的to属性指定要跳转到路由path,组件会被渲染为浏览器支持的a链接,如果需要传参直接通过字符串拼接的方式拼接参数即可
(2)、编程(命令)式导航
编程式导航是指通过 useNavigate
钩子得到导航方法,然后通过调用方法以命令式的形式进行路由跳转,比如想在登录请求完毕之后跳转就可以选择这种方式,更加灵活
4、路由传参
配置路由时:
5、嵌套路由
6、默认二级路由
7、404路由
8、两种路由模式
各个主流框架的路由常用的路由模式有俩种,history模式和hash模式, ReactRouter分别由 createBrowerRouter 和 createHashRouter 函数负责创建
路由模式 | url表现 | 底层原理 | 是否需要后端支持 |
---|---|---|---|
history | url/login | history对象 + pushState事件 | 需要 |
hash | url/#/login | 监听hashChange事件 | 不需要 |
十二、项目细节方法
1、配置别名路径
-
路径解析配置(webpack),把 @/ 解析为 src/
-
路径联想配置(VsCode),VsCode 在输入 @/ 时,自动联想出来对应的 src/下的子级目录
(1)、路径解析配置
配置步骤:
-
安装craco npm i -D @craco/craco
-
项目根目录下创建配置文件 craco.config.js
-
配置文件中添加路径解析配置
-
包文件中配置启动和打包命令
"scripts":{
"start":"craco start",
"build":"craco build"
},
(2)、联想路径配置
配置步骤:
-
根目录下新增配置文件 - jsconfig.json
-
添加路径提示配置
{
"compilerOptions":{
"baseUrl":"./",
"paths":{
"@/*":[
"src/*"
]
}
}
}
2、json-server实现Mock
实现步骤:
1、项目中安装json-server
npm i -D json-server
2、准备一个json文件 (素材里获取) 在根目录下创建server目录,在该目录内创建一个json文件(这里命名为data.json),数据格式如下
{
"ka": [
{
"type": "pay",
"money": -99,
"date": "2022-10-24 10:36:42",
"useFor": "drinks",
"id": 1
},
{
"type": "pay",
"money": -88,
"date": "2022-10-24 10:37:51",
"useFor": "longdistance",
"id": 2
},
{
"type": "income",
"money": 100,
"date": "2022-10-22 00:00:00",
"useFor": "bonus",
"id": 3
}
]
}
3、添加启动命令
在配置文件中(package.json)添加
"server":"json-server ./server/data.json(json文件目录) --port 8888"
4、得到路径
http://localhost:8888/ka
3、antD主题定制
十三、项目实践
1、创建项目
# 使用npx创建项目
npx create-react-app react-jike
# 进入到项
cd react-jike
# 启动项目
npm start
2、项目目录结构
3、scss预处理器
实现步骤
-
安装解析 sass 的包:
npm i sass -D
-
创建全局样式文件:
index.scss
样例代码:
body {
margin: 0;
div {
color: blue;
}
}
4、组件库antd使用
实现步骤
-
安装 antd 组件库:
npm i antd
-
导入 Button 组件
-
在 Login 页面渲染 Button 组件进行测试
5、配置基础路由
实现步骤
-
安装路由包
npm i react-router-dom
-
准备
Layout
和Login
俩个基础组件 -
配置路由
具体见上面
6、配置别名路径
-
安装
craco
工具包 -
增加
craco.config.js
配置文件 -
修改
scripts 命令
-
测试是否生效
npm i @craco/craco -D
具体见上面
7、使用gitee管理项目
用CRA初始化的项目已经在本地创建了本地仓库,如图:
进入到项目目录,关联远程仓库
首次push
8、表单校验
实现步骤
-
为 Form 组件添加
validateTrigger
属性,指定校验触发时机的集合 -
为 Form.Item 组件添加 name 属性
-
为 Form.Item 组件添加
rules
属性,用来添加表单校验规则对象
page/Login/index.js
const Login = () => {
return (
<Form validateTrigger={['onBlur']}>
<Form.Item
name="mobile"
rules={[
{ required: true, message: '请输入手机号' },
{
pattern: /^1[3-9]\d{9}$/,
message: '手机号码格式不对'
}
]}
>
<Input size="large" placeholder="请输入手机号" />
</Form.Item>
<Form.Item
name="code"
rules={[
{ required: true, message: '请输入验证码' },
]}
>
<Input size="large" placeholder="请输入验证码" maxLength={6} />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" size="large" block>
登录
</Button>
</Form.Item>
</Form>
)
}
9、获取登录表单数据
实现步骤
-
为 Form 组件添加
onFinish
属性,该事件会在点击登录按钮时触发 -
创建 onFinish 函数,通过函数参数 values 拿到表单值
-
Form 组件添加
initialValues
属性,来初始化表单值
// 点击登录按钮时触发 参数values即是表单输入数据
const onFinish = formValue => {
console.log(formValue)
}
<Form
onFinish={ onFinish }
>...</Form>
10、封装request工具模块(axios)
-
安装 axios 到项目
-
创建 utils/request.js 文件
-
创建 axios 实例,配置
baseURL,请求拦截器,响应拦截器
-
在 utils/index.js 中,统一导出request
utils/request
import axios from 'axios'
const request= axios.create({
baseURL: 'http://geek.itheima.net/v1_0',
timeout: 5000
})
// 添加请求拦截器
request.interceptors.request.use((config)=> {
return config
}, (error)=> {
return Promise.reject(error)
})
// 添加响应拦截器
request.interceptors.response.use((response)=> {
// 2xx 范围内的状态码都会触发该函数。
// 对响应数据做点什么
return response.data
}, (error)=> {
// 超出 2xx 范围的状态码都会触发该函数。
// 对响应错误做点什么
return Promise.reject(error)
})
export { request }
在utils/index.js中做统一导出
import { request } from './request'
export { request }
11、使用Redux管理token
npm i react-redux @reduxjs/toolkit
user.js
import { createSlice } from '@reduxjs/toolkit'
import { http } from '@/utils'
const userStore = createSlice({
name: 'user',
// 数据状态
initialState: {
token:''
},
// 同步修改方法
reducers: {
setUserInfo (state, action) {
state.token = action.payload
}
}
})
// 解构出actionCreater
const { setUserInfo } = userStore.actions
// 获取reducer函数
const userReducer = userStore.reducer
// 异步方法封装
const fetchLogin = (loginForm) => {
return async (dispatch) => {
const res = await http.post('/authorizations', loginForm)
dispatch(setUserInfo(res.data.token))
}
}
export { fetchLogin }
export default userReducer
index.js
import { configureStore } from '@reduxjs/toolkit'
import userReducer from './modules/user'
export default configureStore({
reducer: {
// 注册子模块
user: userReducer
}
})
12、登录逻辑
import { message } from 'antd'
import useStore from '@/store'
import { fetchLogin } from '@/store/modules/user'
import { useDispatch } from 'react-redux'
const Login = () => {
const dispatch = useDispatch()
const navigate = useNavigate()
const onFinish = async formValue => {
await dispatch(fetchLogin(formValue))
navigate('/')
message.success('登录成功')
}
return (
<div className="login">
<!-- 省略... -->
</div>
)
}
export default Login
13、持久化token
(1)、封装存取方法
utils/token.js
const TOKENKEY = 'token_key'
function setToken (token) {
return localStorage.setItem(TOKENKEY, token)
}
function getToken () {
return localStorage.getItem(TOKENKEY)
}
function clearToken () {
return localStorage.removeItem(TOKENKEY)
}
export {
setToken,
getToken,
clearToken
}
在utils/index.js中统一导出
(2)、实现持久化
import { getToken, setToken } from '@/utils'
const userStore = createSlice({
name: 'user',
// 数据
initialState: {
token: getToken() || ''
},
// 同步修改方法
reducers: {
setUserInfo (state, action) {
state.token = action.payload
// 存入本地
setToken(state.token)
}
}
})
14、请求拦截器注入token
业务背景: Token作为用户的数据标识,在接口层面起到了接口权限控制的作用,也就是说后端有很多接口都需要通过查看当前请求头信息中是否含有token数据,来决定是否正常返回数据
utils/request.js
// 添加请求拦截器
request.interceptors.request.use(config => {
// if not login add token
const token = getToken()
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
15、路由鉴权实现
业务背景:封装
AuthRoute
路由鉴权高阶组件,实现未登录拦截,并跳转到登录页面 实现思路:判断本地是否有token,如果有,就返回子组件,否则就重定向到登录Login
实现步骤
-
在 components 目录中,创建
AuthRoute/index.jsx
文件 -
登录时,直接渲染相应页面组件
-
未登录时,重定向到登录页面
-
将需要鉴权的页面路由配置,替换为 AuthRoute 组件渲染
代码实现 components/AuthRoute/index.jsx
import { getToken } from '@/utils'
import { Navigate } from 'react-router-dom'
const AuthRoute = ({ children }) => {
const isToken = getToken()
if (isToken) {
return <>{children}</>
} else {
return <Navigate to="/login" replace />
}
}
export default AuthRoute
src/router/index.jsx
import { createBrowserRouter } from 'react-router-dom'
import Login from '@/pages/Login'
import Layout from '@/pages/Layout'
import AuthRoute from '@/components/Auth'
const router = createBrowserRouter([
{
path: '/',
element: <AuthRoute><Layout /></AuthRoute>,
},
{
path: '/login',
element: <Login />,
},
])
export default router
16、样式的reset
取消默认样式
npm install normalize.css
在入口文件中index.js
import 'normalize.css'
根据业务需求,再写相关的样式
例如
html,
body {
margin: 0;
height: 100%;
}
#root {
height: 100%;
}
17、二级路由
import { createBrowserRouter } from 'react-router-dom'
import Login from '@/pages/Login'
import Layout from '@/pages/Layout'
import Publish from '@/pages/Publish'
import Article from '@/pages/Article'
import Home from '@/pages/Home'
import { AuthRoute } from '@/components/Auth'
const router = createBrowserRouter([
{
path: '/',
element: (
<AuthRoute>
<Layout />
</AuthRoute>
),
children: [
{
index: true,
element: <Home />,
},
{
path: 'article',
element: <Article />,
},
{
path: 'publish',
element: <Publish />,
},
],
},
{
path: '/login',
element: <Login />,
},
])
export default router
二级路由跳转
import { Outlet, useNavigate } from 'react-router-dom'
const items = [
{
label: '首页',
key: '/',
icon: <HomeOutlined />,
},
{
label: '文章管理',
key: '/article',
icon: <DiffOutlined />,
},
{
label: '创建文章',
key: '/publish',
icon: <EditOutlined />,
},
]
const GeekLayout = () => {
const navigate = useNavigate()
const menuClick = (route) => {
navigate(route.key)
}
return (
<Menu
mode="inline"
theme="dark"
selectedKeys={selectedKey}
items={items}
style={{ height: '100%', borderRight: 0 }}
onClick={menuClick}
/>
)
}
export default GeekLayout
18、菜单反向高亮
useLocation()钩子函数获取url路径
const GeekLayout = () => {
// 省略部分代码
const location = useLocation()
const selectedKey = location.pathname
return (
<Layout>
<Header className="header">
<div className="logo" />
<div className="user-info">
<span className="user-name">{name}</span>
<span className="user-logout">
<Popconfirm title="是否确认退出?" okText="退出" cancelText="取消">
<LogoutOutlined /> 退出
</Popconfirm>
</span>
</div>
</Header>
<Layout>
<Sider width={200} className="site-layout-background">
<Menu
mode="inline"
theme="dark"
selectedKeys={selectedKey} //绑定路径
items={items}
style={{ height: '100%', borderRight: 0 }}
onClick={menuClickHandler}></Menu>
</Sider>
<Layout className="layout-content" style={{ padding: 20 }}>
<Outlet />
</Layout>
</Layout>
</Layout>
)
}
19、退出登录实现
实现步骤
-
为气泡确认框添加确认回调事件
-
在
store/userStore.js
中新增退出登录的action函数,在其中删除token -
在回调事件中,调用userStore中的退出action
-
清除用户信息,返回登录页面
代码实现 store/modules/user.js
清空信息
import { createSlice } from '@reduxjs/toolkit'
import { http } from '@/utils/request'
import { clearToken, getToken, setToken } from '@/utils'
const userStore = createSlice({
name: 'user',
// 数据
initialState: {
token: getToken() || '',
userInfo: {}
},
// 同步修改方法
reducers: {
setUserToken (state, action) {
state.token = action.payload
// 存入本地
setToken(state.token)
},
setUserInfo (state, action) {
state.userInfo = action.payload
},
clearUserInfo (state) {
state.token = ''
state.userInfo = {}
clearToken()
}
}
})
// 解构出actionCreater
const { setUserToken, setUserInfo, clearUserInfo } = userStore.actions
// 获取reducer函数
const userReducer = userStore.reducer
export { fetchLogin, fetchUserInfo, clearUserInfo }
export default userReducer
pages/Layout/index.js
const GeekLayout = () => {
// 退出登录
const loginOut = () => {
dispatch(clearUserInfo())
navigator('/login')
}
return (
<Layout>
<Header className="header">
<div className="logo" />
<div className="user-info">
<span className="user-name">{name}</span>
<span className="user-logout">
<Popconfirm
title="是否确认退出?"
okText="退出"
cancelText="取消"
onConfirm={loginOut}>
<LogoutOutlined /> 退出
</Popconfirm>
</span>
</div>
</Header>
<Layout>
<Sider width={200} className="site-layout-background">
<Menu
mode="inline"
theme="dark"
selectedKeys={selectedKey}
items={items}
style={{ height: '100%', borderRight: 0 }}
onClick={menuClickHandler}></Menu>
</Sider>
<Layout className="layout-content" style={{ padding: 20 }}>
<Outlet />
</Layout>
</Layout>
</Layout>
)
}
20、token失效
业务背景:如果用户一段时间不做任何操作,到时之后应该清除所有过期用户信息跳回到登录
request.interceptors.response.use((response) => {
// 2xx 范围内的状态码都会触发该函数。
// 对响应数据做点什么
return response.data
}, (error) => {
// 超出 2xx 范围的状态码都会触发该函数。
// 对响应错误做点什么
//console.dir(error)
if (error.response.status === 401) {
clearToken()
router.navigate('/login')
window.location.reload()
}
return Promise.reject(error)
})
21、echarts
(1)、Demo
安装echarts
npm i echarts
实现基础Demo
import { useEffect, useRef } from 'react'
import * as echarts from 'echarts'
const Home = () => {
const chartRef = useRef(null)
useEffect(() => {
// 1. 生成实例
const myChart = echarts.init(chartRef.current)
// 2. 准备图表参数
const option = {
xAxis: {
type: 'category',
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
},
yAxis: {
type: 'value'
},
series: [
{
data: [120, 200, 150, 80, 70, 110, 130],
type: 'bar'
}
]
}
// 3. 渲染参数
myChart.setOption(option)
}, [])
return (
<div>
<div ref={chartRef} style={{ width: '400px', height: '300px' }} />
</div >
)
}
export default Home
(2)、组件封装
import { useRef, useEffect } from 'react'
import * as echarts from 'echarts'
const BarChart = ({ xData, sData, style = { width: '400px', height: '300px' } }) => {
const chartRef = useRef(null)
useEffect(() => {
// 1. 生成实例
const myChart = echarts.init(chartRef.current)
// 2. 准备图表参数
const option = {
xAxis: {
type: 'category',
data: xData
},
yAxis: {
type: 'value'
},
series: [
{
data: sData,
type: 'bar'
}
]
}
// 3. 渲染参数
myChart.setOption(option)
}, [sData, xData])
return <div ref={chartRef} style={style}></div>
}
export { BarChart }
import { BarChart } from './BarChart'
const Home = () => {
return (
<div>
<BarChart
xData={['Vue', 'React', 'Angular']}
sData={[2000, 5000, 1000]} />
<BarChart
xData={['Vue', 'React', 'Angular']}
sData={[200, 500, 100]}
style={{ width: '500px', height: '400px' }} />
</div >
)
}
export default Home
22、aip封装
将请求统一封装成函数到apis中
apis/user.js
23、富文本编辑器
特定版本的react-quill,兼容react18
npm i react-quill@2.0.0-beta.2 --legacy-peer-deps
导入资源渲染组件
import ReactQuill from 'react-quill'
import 'react-quill/dist/quill.snow.css'
const Publish = () => {
return (
// ...
<Form
labelCol={{ span: 4 }}
wrapperCol={{ span: 16 }}
>
<Form.Item
label="内容"
name="content"
rules={[{ required: true, message: '请输入文章内容' }]}
>
<ReactQuill
className="publish-quill"
theme="snow"
placeholder="请输入文章内容"
/>
</Form.Item>
</Form>
)
}
.publish-quill {
.ql-editor {
min-height: 300px;
}
}
24、图片上传
<Form.Item label="封面">
<Form.Item name="type">
<Radio.Group>
<Radio value={1}>单图</Radio>
<Radio value={3}>三图</Radio>
<Radio value={0}>无图</Radio>
</Radio.Group>
</Form.Item>
<Upload
listType="picture-card"
showUploadList
>
<div style={{ marginTop: 8 }}>
<PlusOutlined />
</div>
</Upload>
</Form.Item>
(1)、实现基础上传
实现步骤
-
为 Upload 组件添加
action 属性
,配置封面图片上传接口地址 -
为 Upload组件添加
name属性
, 接口要求的字段名 -
为 Upload 添加
onChange 属性
,在事件中拿到当前图片数据,并存储到React状态中
代码实现
import { useState } from 'react'
const Publish = () => {
// 上传图片
const [imageList, setImageList] = useState([])
const onUploadChange = (info) => {
setImageList(info.fileList)
}
return (
<Form.Item label="封面">
<Form.Item name="type">
<Radio.Group>
<Radio value={1}>单图</Radio>
<Radio value={3}>三图</Radio>
<Radio value={0}>无图</Radio>
</Radio.Group>
</Form.Item>
<Upload
name="image"
listType="picture-card"
showUploadList
action={'http://geek.itheima.net/v1_0/upload'}
onChange={onUploadChange}
>
<div style={{ marginTop: 8 }}>
<PlusOutlined />
</div>
</Upload>
</Form.Item>
)
}
(2)、切换图片Type
实现步骤
-
点击单选框时拿到当前的类型value
-
根据value控制上传组件的显示(大于零时才显示)
const Publish = ()=>{
// 控制图片Type
const [imageType, setImageType] = useState(0)
const onTypeChange = (e) => {
console.log(e)
setImageType(e.target.value)
}
return (
<FormItem>
<Radio.Group onChange={onTypeChange}>
<Radio value={1}>单图</Radio>
<Radio value={3}>三图</Radio>
<Radio value={0}>无图</Radio>
</Radio.Group>
{imageType > 0 &&
<Upload
name="image"
listType="picture-card"
showUploadList
action={'http://geek.itheima.net/v1_0/upload'}
onChange={onUploadChange}
>
<div style={{ marginTop: 8 }}>
<PlusOutlined />
</div>
</Upload>}
</FormItem>
)
}
(3)、限制图片数量
实现步骤
-
通过 maxCount 属性限制图片的上传图片数量
{imageType > 0 &&
<Upload
name="image"
listType="picture-card"
className="avatar-uploader"
showUploadList
action={'http://geek.itheima.net/v1_0/upload'}
onChange={onUploadChange}
maxCount={imageType} //最大数量
multiple={imageType > 1}
>
<div style={{ marginTop: 8 }}>
<PlusOutlined />
</div>
</Upload>}
25、自定义hook
步骤
编写获取频道列表的hook
src/hooks/useChannel.js
使用
26、编辑文章跳转
代码实现
const columns = [
// ...
{
title: '操作',
render: data => (
<Space size="middle">
<Button
type="primary"
shape="circle"
icon={<EditOutlined />}
onClick={() => navagite(`/publish?id=${data.id}`)} />
/>
</Space>
)
}
]
根据路径参数回填数据
const Publish = ()=>{
// 回填数据
const [searchParams] = useSearchParams()
const articleId = searchParams.get('id')
const [form] = Form.useForm()
useEffect(() => {
async function getArticle () {
const res = await http.get(`/mp/articles/${articleId}`)
const { cover, ...formValue } = res.data
// 设置表单数据
form.setFieldsValue({ ...formValue, type: cover.type })
}
if (articleId) {
// 拉取数据回显
getArticle()
}
}, [articleId, form])
return (
<Form form={form}/>
)
}
图片
useEffect(() => {
async function getArticle () {
const res = await http.get(`/mp/articles/${articleId}`)
const { cover, ...formValue } = res.data
// 1. 回填表单数据
form.setFieldsValue({ ...formValue, type: cover.type })
// 2. 回填封面图片
setImageType(cover.type) // 封面类型
setImageList(cover.images.map(url => ({ url }))) // 封面list
}
if (articleId) {
getArticle()
}
}, [articleId, form])
十四、项目打包
1、项目打包
npm run build
根目录下生成build文件夹
2、项目本地预览
实现步骤
-
全局安装本地服务包
npm i -g serve
该包提供了serve命令,用来启动本地服务器 -
在项目根目录中执行命令
serve -s ./build
在build目录中开启服务器 -
在浏览器中访问:
http://localhost:3000/
预览项目
3、优化-路由懒加载
使用步骤
-
使用 lazy 方法导入路由组件
-
使用内置的 Suspense 组件渲染路由组件
import { createBrowserRouter } from 'react-router-dom'
import { lazy, Suspense } from 'react'
import Login from '@/pages/Login'
import Layout from '@/pages/Layout'
import AuthRoute from '@/components/Auth'
const Publish = lazy(() => import('@/pages/Publish'))
const Article = lazy(() => import('@/pages/Article'))
const Home = lazy(() => import('@/pages/Article'))
const router = createBrowserRouter([
{
path: '/',
element: (
<AuthRoute>
<Layout />
</AuthRoute>
),
children: [
{
index: true,
element: (
<Suspense fallback={'加载中'}>
<Home />
</Suspense>
)
},
{
path: 'article',
element: (
<Suspense fallback={'加载中'}>
<Article />
</Suspense>
)
},
{
path: 'publish',
element: (
<Suspense fallback={'加载中'}>
<Publish />
</Suspense>
)
},
],
},
{
path: '/login',
element: <Login />,
},
])
export default router
4、打包-打包体积分析
业务背景 通过分析打包体积,才能知道项目中的哪部分内容体积过大,方便知道哪些包如何来优化 使用步骤
-
安装分析打包体积的包:
npm i source-map-explorer
-
在 package.json 中的 scripts 标签中,添加分析打包体积的命令
-
对项目打包:
npm run build
(如果已经打过包,可省略这一步) -
运行分析命令:
npm run analyze
-
通过浏览器打开的页面,分析图表中的包体积
核心代码:
package.json
"scripts": {
"analyze": "source-map-explorer 'build/static/js/*.js'",
}
5、优化-配置CDN
分析说明:通过 craco 来修改 webpack 配置,从而实现 CDN 优化 核心代码
craco.config.js
// 添加自定义对于webpack的配置
const path = require('path')
const { whenProd, getPlugin, pluginByName } = require('@craco/craco')
module.exports = {
// webpack 配置
webpack: {
// 配置别名
alias: {
// 约定:使用 @ 表示 src 文件所在路径
'@': path.resolve(__dirname, 'src')
},
// 配置webpack
// 配置CDN
configure: (webpackConfig) => {
let cdn = {
js:[]
}
whenProd(() => {
// key: 不参与打包的包(由dependencies依赖项中的key决定)
// value: cdn文件中 挂载于全局的变量名称 为了替换之前在开发环境下
webpackConfig.externals = {
react: 'React',
'react-dom': 'ReactDOM'
}
// 配置现成的cdn资源地址
// 实际开发的时候 用公司自己花钱买的cdn服务器
cdn = {
js: [
'https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.production.min.js',
'https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.production.min.js',
]
}
})
// 通过 htmlWebpackPlugin插件 在public/index.html注入cdn资源url
const { isFound, match } = getPlugin(
webpackConfig,
pluginByName('HtmlWebpackPlugin')
)
if (isFound) {
// 找到了HtmlWebpackPlugin的插件
match.userOptions.files = cdn
}
return webpackConfig
}
}
}
public/index.html
<body>
<div id="root"></div>
<!-- 加载第三发包的 CDN 链接 -->
<% htmlWebpackPlugin.options.files.js.forEach(cdnURL => { %>
<script src="<%= cdnURL %>"></script>
<% }) %>
</body>
十五、useReducer
// 定义reducer
import { useReducer } from 'react'
// 1. 根据不同的action返回不同的新状态
function reducer(state, action) {
console.log('reducer执行了')
switch (action.type) {
case 'INC':
return state + 1
case 'DEC':
return state - 1
case 'UPDATE':
return state + action.payload
default:
return state
}
}
function App() {
// 2. 使用useReducer分派action
const [state, dispatch] = useReducer(reducer, 0)
return (
<>
{/* 3. 调用dispatch函数传入action对象 触发reducer函数,分派action操作,使用新状态更新视图 */}
<button onClick={() => dispatch({ type: 'DEC' })}>-</button>
{state}
<button onClick={() => dispatch({ type: 'INC' })}>+</button>
<button onClick={() => dispatch({ type: 'UPDATE', payload: 100 })}>
update to 100
</button>
</>
)
}
export default App
十六、渲染性能优化
1、useMemo
作用:它在每次重新渲染的时候能够缓存计算的结果
思路: 只有count发生变化时才重新进行计算
import { useMemo, useState } from 'react'
function fib (n) {
console.log('计算函数执行了')
if (n < 3) return 1
return fib(n - 2) + fib(n - 1)
}
function App() {
const [count, setCount] = useState(0)
// 计算斐波那契之和
// const sum = fib(count)
// 通过useMemo缓存计算结果,只有count发生变化时才重新计算
const sum = useMemo(() => {
return fib(count)
}, [count])
const [num, setNum] = useState(0)
return (
<>
{sum}
<button onClick={() => setCount(count + 1)}>+count:{count}</button>
<button onClick={() => setNum(num + 1)}>+num:{num}</button>
</>
)
}
export default App
2、React.memo
作用:允许组件在props没有改变的情况下跳过重新渲染
import React, { useState } from 'react'
const MemoSon = React.memo(function Son() {
console.log('子组件被重新渲染了')
return <div>this is span</div>
})
function App() {
const [, forceUpdate] = useState()
console.log('父组件重新渲染了')
return (
<>
<MemoSon />
<button onClick={() => forceUpdate(Math.random())}>update</button>
</>
)
}
export default App
引用稳定,用useMemo函数
3、useCallback
上一小节我们说到,当给子组件传递一个引用类型
prop的时候,即使我们使用了memo
函数依旧无法阻止子组件的渲染,其实传递prop的时候,往往传递一个回调函数更为常见,比如实现子传父,此时如果想要避免子组件渲染,可以使用 useCallback
缓存回调函数
// useCallBack
import { memo, useCallback, useState } from 'react'
const MemoSon = memo(function Son() {
console.log('Son组件渲染了')
return <div>this is son</div>
})
function App() {
const [, forceUpate] = useState()
console.log('父组件重新渲染了')
const onGetSonMessage = useCallback((message) => {
console.log(message)
}, [])
return (
<div>
<MemoSon onGetSonMessage={onGetSonMessage} />
<button onClick={() => forceUpate(Math.random())}>update</button>
</div>
)
}
export default App
十七、forwardRef
作用:允许组件使用ref将一个DOM节点暴露给父组件
import { forwardRef, useRef } from 'react'
const MyInput = forwardRef(function Input(props, ref) {
return <input {...props} type="text" ref={ref} />
}, [])
function App() {
const ref = useRef(null)
const focusHandle = () => {
console.log(ref.current.focus())
}
return (
<div>
<MyInput ref={ref} />
<button onClick={focusHandle}>focus</button>
</div>
)
}
export default App
十八、useImperativeHandle
作用:如果我们并不想暴露子组件中的DOM而是想暴露子组件内部的方法
import { forwardRef, useImperativeHandle, useRef } from 'react'
const MyInput = forwardRef(function Input(props, ref) {
// 实现内部的聚焦逻辑
const inputRef = useRef(null)
const focus = () => inputRef.current.focus()
// 暴露子组件内部的聚焦方法
useImperativeHandle(ref, () => {
return {
focus,
}
})
return <input {...props} ref={inputRef} type="text" />
})
function App() {
const ref = useRef(null)
const focusHandle = () => ref.current.focus()
return (
<div>
<MyInput ref={ref} />
<button onClick={focusHandle}>focus</button>
</div>
)
}
export default App
十九、Class API
1、基础使用
// class API
import { Component } from 'react'
class Counter extends Component {
// 状态变量
state = {
count: 0,
}
// 事件回调
clickHandler = () => {
// 修改状态变量 触发UI组件渲染
this.setState({
count: this.state.count + 1,
})
}
// UI模版
render() {
return <button onClick={this.clickHandler}>+{this.state.count}</button>
}
}
function App() {
return (
<div>
<Counter />
</div>
)
}
export default App
2、生命周期函数
3、组件通信
(1)、父传子
(2)、子传父
二十、zustand
npm i zustand
1、快速上手
store/index.js - 创建store
import { create } from 'zustand'
const useStore = create((set) => {
return {
count: 0,
inc: () => {
set(state => ({ count: state.count + 1 }))
}
}
})
export default useStore
app.js - 绑定组件
import useStore from './store/useCounterStore.js'
function App() {
const { count, inc } = useStore()
return <button onClick={inc}>{count}</button>
}
export default App
2、异步支持
对于异步操作的支持不需要特殊的操作,直接在函数中编写异步逻辑,最后把接口的数据放到set函数中返回即可
store/index.js - 创建store
import { create } from 'zustand'
const URL = 'http://geek.itheima.net/v1_0/channels'
const useStore = create((set) => {
return {
count: 0,
ins: () => {
return set(state => ({ count: state.count + 1 }))
},
channelList: [],
fetchChannelList: async () => {
const res = await fetch(URL)
const jsonData = await res.json()
set({channelList: jsonData.data.channels})
}
}
})
export default useStore
app.js - 绑定组件
import { useEffect } from 'react'
import useChannelStore from './store/channelStore'
function App() {
const { channelList, fetchChannelList } = useChannelStore()
useEffect(() => {
fetchChannelList()
}, [fetchChannelList])
return (
<ul>
{channelList.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
)
}
export default App
3、切片模式
import { create } from 'zustand'
// 创建counter相关切片
const createCounterStore = (set) => {
return {
count: 0,
setCount: () => {
set(state => ({ count: state.count + 1 }))
}
}
}
// 创建channel相关切片
const createChannelStore = (set) => {
return {
channelList: [],
fetchGetList: async () => {
const res = await fetch(URL)
const jsonData = await res.json()
set({ channelList: jsonData.data.channels })
}
}
}
// 组合切片
const useStore = create((...a) => ({
...createCounterStore(...a),
...createChannelStore(...a)
}))
使用方法不变
二十一、React和TypeScript
1、环境搭建(vite)
npm create vite@latest 项目名称 -- --template react-ts
# 安装依赖
npm i
# 运行项目
npm run dev
2、Hooks与TypeScript
(1)、useState
简单场景
简单场景下,可以使用TS的自动推断机制,不用特殊编写类型注解,运行良好
const [val, toggle] = React.useState(false)
// `val` 会被自动推断为布尔类型
// `toggle` 方法调用时只能传入布尔类型
复杂场景
复杂数据类型,useState支持通过
泛型参数
指定初始参数类型以及setter函数的入参类型
type User = {
name: string
age: number
}
const [user, setUser] = React.useState<User>({
name: 'jack',
age: 18
})
// 执行setUser
setUser(newUser)
// 这里newUser对象只能是User类型
没有具体默认值
实际开发时,有些时候useState的初始值可能为null或者undefined,按照泛型的写法是不能通过类型校验的,此时可以通过完整的类型联合null或者undefined类型即可
type User = {
name: String
age: Number
}
const [user, setUser] = React.useState<User>(null)
// 上面会类型错误,因为null并不能分配给User类型
const [user, setUser] = React.useState<User | null>(null)
// 上面既可以在初始值设置为null,同时满足setter函数setUser的参数可以是具体的User类型
(2)、useRef
在TypeScript的环境下,
useRef
函数返回一个只读
或者可变
的引用,只读的场景常见于获取真实dom,可变的场景,常见于缓存一些数据,不跟随组件渲染,下面分俩种情况说明
获取dom
获取DOM时,通过泛型参数指定具体的DOM元素类型即可
function Foo() {
// 尽可能提供一个具体的dom type, 可以帮助我们在用dom属性时有更明确的提示
// divRef的类型为 RefObject<HTMLDivElement>
const inputRef = useRef<HTMLDivElement>(null)
useEffect(() => {
inputRef.current?.focus()
})
return <div ref={inputRef}>etc</div>
}
如果你可以确保divRef.current
不是null,也可以在传入初始值的位置
// 添加非null标记
const divRef = useRef<HTMLDivElement>(null!)
// 不再需要检查`divRef.current` 是否为null
doSomethingWith(divRef.current)
稳定引用存储器
当做为可变存储容器使用的时候,可以通过
泛型参数
指定容器存入的数据类型, 在还为存入实际内容时通常把null作为初始值,所以依旧可以通过联合类型做指定
interface User {
age: number
}
function App(){
const timerRef = useRef<number | undefined>(undefined)
const userRes = useRef<User | null> (null)
useEffect(()=>{
timerRef.current = window.setInterval(()=>{
console.log('测试')
},1000)
return ()=>clearInterval(timerRef.current)
})
return <div> this is app</div>
}
3、Props与TS
(1)、基础使用
(2)、children属性
(3)、为实践prop添加类型
二十二、React-TypeScrip项目注意事项(Vite)
省略的部分同第十三项
1、初始化
npm create vite@latest 项目名 -- --template react-ts
npm i
npm run dev
初始化仓库
git init .
2、 antd-mobile
3、配置基础路由
4、配置路径别名
修改vite.config.ts配置
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
})
安装node类型包
npm i @types/node -D
修改tsconfig.json文件
{
"baseUrl": ".",
"paths": {
"@/*": [
"src/*"
]
},
}
直接放对象里就行
5、安装axios,简单封装,统一导出
6、API模块
封装泛型
export type ResType<T> = {
message: string
data: T
}
封装请求函数
import { http } from '@/utils'
import type { ResType } from './shared'
type ChannelRes = {
channels: { id: number; name: string }[]
}
export function fetchChannelAPI() {
return http.request<ResType<ChannelRes>>({
url: '/channels',
})
}
测试API函数
fetchChannelAPI().then((res) => { console.log(res.data.data.channels)})