2025-03-29 14:35:49 +08:00

815 lines
21 KiB
Markdown
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Redux
> 状态管理
> 适合大型项目
> 类似的还有zustand, mobx, mobx-react-lite
## hello redux
```sh
npm install react-redux
```
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-y/redux/4.1.2/redux.min.js"></script>
<div>
<button id="sub">减少</button>
<span id="count">1</span>
<button id="add">增加</button>
</div>
<script>
const subBtn = document.getElementById('sub')
const addBtn = document.getElementById('add')
const countSpan = document.getElementById('count')
/** 网页中使用redux
* 1. 引入redux包
* 2. 创建reducer整合函数
* 3. 通过reducer对象创建store
* 4. 对store中的state进行订阅
* 5. 通过dispatch派发state的操作指令
* */
/**
* state 当前的state, 根据这个state生成新的state
* action 是一个对象, 保存操作的信息
*/
function reducer(state={count:0}, action){
switch (action.type){
case 'Add':
return {...state, count:state.count +1}
case 'Sub':
return {...state, count:state.count -1}
case 'Add_N':
return {...state, count:state.count +action.payload}
default:
return state
}
}
/**
* 第一个参数是派发器,第二个参数是初始值(也可以在reducer设置初始值)
*/
const store = Redux.createStore(reducer, {count:1})
// 订阅, 其实就是回调, 发生变化的时候执行
store.subscribe(()=>{
// console.log(store.getState())
countSpan.innerText = store.getState().count
})
let count = 1
subBtn.addEventListener('click',()=>{
// count--
// countSpan.innerText = count
store.dispatch({type: 'Sub'})
})
addBtn.addEventListener('click',()=>{
// count++
// countSpan.innerText = count
store.dispatch({type: 'Add'})
})
</script>
</body>
</html>
```
## Redux Toolkit (RTK)
[Quick Start | Redux Toolkit](https://redux-toolkit.js.org/tutorials/quick-start)
```
问题:
1.如果state过干复杂将会非常难以维护
- 可以通过对state 分组为解次这个向题创建多个reducer然后將其合并为一个
2.state每次操作时都需要对state进行复制然后再去修改
3.case后边的常量维护起来会比较麻烦
```
```bash
# 安装
npm install react-redux @reduxjs/toolkit -S
```
```js
/**
* createSlice:创建reducer(归纳函数)切片
* - 名字, 用来自动生成action.type
* - 初始值
* - reducers: 放更新state的方法
* - 方法有两个参数: state和action
* state是一个代理对象,可以直接修改
*
* 切片会自动生成actions, 打印actions可以发现存储多个函数:actionCreator() action创建器
*
* 调用action创建器后会自动创建action对象
* action对象的结构 {type: 'name/函数名', payload: '函数的参数'}
*
* 创建store: configureStore(option)
* - {reducer: {名字:切片的reducer}}
*/
```
示例:
```js
// @/store/index.js
// 创建store
import { createSlice, configureStore } from '@reduxjs/toolkit'
// 初始值
const initialState = {
name: '唐僧',
age: 30,
gender: '男',
address: '长安',
}
// 创建切片, 切片会自动生成action和reducer
export const stuSlice = createSlice({
name: 'stu',
initialState,
reducers: {
setName: (state, action) => {
state.name = action.payload
},
setAge: (state, action) => {
state.age = action.payload
},
},
})
console.log(stuSlice.actions)
// actionCreator() action创建器
export const { setName, setAge } = stuSlice.actions
// 把action创建器暴露出去, 代替原来手写的{type,action}
const nameAction = setName('猪八戒')
console.log(nameAction)
// {type: 'stu/stuName', payload: '猪八戒'}
// 调用action创建器生成action对象
const store = configureStore({
reducer: {
student: stuSlice.reducer,
// reducer也是切片自动创建的
// 这里的student名字在引入store的时候要用到,切片可能有很多个,用名字取其中一个
},
})
console.log(store)
export default store
```
怎么使用:
```
// 1. 用redux提供的Provider把整个app包起来
// 和react自带的Provider的区别是这里传入的是store
```
```js
// @/index.js
import { Provider } from 'react-redux';
import store from './store'
// ...
root.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
)
//...
```
```
// 2.在组件中使用
// 通过useSelector()钩子获取state, 通过useDispatch()获取派发器对象
// 修改state的时候,派发器传入切片自动生成的action创建器
// action创建器里的值是要修改的值
// 当然也可以手动传如action:格式 {type: 'stu/stuName', payload: '猪八戒'},但不推荐,容易出错
```
```js
// @/app.js
import { useSelector, useDispatch } from 'react-redux'
import {useState} from 'react'
import {setName} from './store'
import './App.css'
function App() {
// 通过useSelector()钩子获取state
// 默认state是所有的,可以只取一部分
const stu = useSelector(state => state.student)
// 这里的名字是configureStore参数里reducer对应的名字
// 通过useDispatch()获取派发器对象
const dispatch = useDispatch()
const [inputName, setInputName] = useState('')
const nameChangeHandler = (e)=>{
e.preventDefault()
setInputName(e.target.value)
}
const saveNameHandler = ()=>{
// dispatch传入切片自动生成的action
dispatch(setName(inputName))
}
return (
<div className="App">
<p>姓名: {stu.name}</p>
<p>年龄: {stu.age}</p>
<p>性别: {stu.gender}</p>
<p>地址: {stu.address}</p>
<p>
<input type="text" value={inputName} onChange={e=>nameChangeHandler(e)}/>
<button onClick={saveNameHandler}>修改名字</button>
</p>
</div>
)
}
export default App
```
### 拆分模块
实际使用不会把切片都写在index里, 一般拆分开, 稍微修改下上面的代码
```js
// 新建@/store/stuSlice.js
import { createSlice } from '@reduxjs/toolkit'
const initialState = {
name: '唐僧',
age: 30,
gender: '男',
address: '长安',
}
export const stuSlice = createSlice({
name: 'stu',
initialState,
reducers: {
setName: (state, action) => {
state.name = action.payload
},
setAge: (state, action) => {
state.age = action.payload
},
},
})
export const { setName, setAge } = stuSlice.actions
export const {reducer:stuReducer} = stuSlice
```
```js
// @/store/index.js也要改下
import { configureStore } from '@reduxjs/toolkit'
import {stuReducer} from './stuSlice'
const store = configureStore({
reducer: {
student: stuReducer,
},
})
export default store
```
```js
// 引入的部分也要改下
// @/app.js
import { useSelector, useDispatch } from 'react-redux'
import {setName as setStuName} from './store/stuSlice'
// ...
const {student:stu} = useSelector(state => state)
const dispatch = useDispatch()
const saveNameHandler = ()=>{
// dispatch传入切片自动生成的action
dispatch(setName(inputName))
}
//...
```
## RTKQ
[RTK Query | Redux Toolkit](https://redux-toolkit.js.org/tutorials/rtk-query)
> Q: query, 简化在 Web 应用程序中加载数据的常见情况
### example
#### 1 . 创建模块
```js
// 创建rtkq中的api对象,会自动根据各种方法生成对应的钩子函数
// 参数:
// - reducerPath: 标识
// - baseQuery: 指定查询的基本信息, 发送请求使用的工具
// - endpoints: 指定api的各种功能, build: 请求的构建器
// - query 查询的方法,指定请求的路径
// - mutation 更新的方法
// - (可选)transformResponse 对响应数据进行处理
// 导出钩子命名规则: getStuList => useGetStuListQuery use表示钩子,Query表示查询方法
```
```js
// src/store/stuApi.js
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
export const stuApi = createApi({
reducerPath: 'stuApi',
baseQuery: fetchBaseQuery({ baseUrl: 'http://localhost:1337/api/' }),
endpoints: (build) => ({
getStuList: build.query({
query: () => 'students',
// transformResponse对响应数据进行处理
transformResponse(res){return res.data}
}),
getStuInfoById: build.query({
query: (id) => `students/${id}`, // 接收路径参数
}),
updateStuById: build.mutation({
query: ({ id, stu }) => {
return {
url: `students/${id}`,
method: 'put',
body: { data: stu },
}
},
}),
}),
})
export const {useGetStuListQuery} = stuApi
```
#### 2 . 配置 store
```js
// src/store/index.js
import { configureStore } from '@reduxjs/toolkit'
import { stuApi } from './stuApi'
// 添加到store
export const store = configureStore({
reducer: {
// api对象的路径名当store的标识
// api对象的reducer自动生成的
[stuApi.reducerPath]: stuApi.reducer,
},
// 添加中间件用来实现缓存, 失效, 轮寻
// 必须添加
middleware: getDefaultMiddleware => {
return getDefaultMiddleware().concat(stuApi.middleware)
},
})
```
#### 3 . 注入 store
```jsx
// src/main.jsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import { Provider } from 'react-redux'
import App from './App'
import './index.css'
import store from './store'
ReactDOM.createRoot(document.getElementById('root')).render(
<Provider store={store}>
<App />
</Provider>
)
```
#### 4 . 使用 api
```jsx
import { useState } from 'react'
import './App.css'
import { useGetStuListQuery } from './store/stuApi'
function App() {
// 调用钩子查询数据
// 钩子返回一个对象,包括请求过程中相关的数据
const obj = useGetStuListQuery()
console.log(obj)
const { isLoading, isSuccess, data} = obj
return (
<div className="App">
{isLoading && <p>--数据正在加载--</p>}
{isSuccess &&
data.map(item=>{
return <p key={item.id}>
姓名: {item.attributes.name} |
性别: {item.attributes.gender==='male'?'男':'女'} |
年龄: {item.attributes.age} |
地址: {item.attributes.address}
</p>
})
}
</div>
)
}
export default App
```
### `keepUnusedDataFor`:缓存功能
>rtkq会查询数据后会默认缓存60秒, 在期间不重复发请求而是使用缓存
设置缓存时间`keepUnusedDataFor`
```js
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
endpoints: (build) => ({
getPosts: build.query({
query: () => 'posts',
keepUnusedDataFor: 5, // 单位是秒,0为不缓存
}),
}),
})
```
### useQuery返回的对象
```js
/**
* refetch: 用来重新加载数据的函数
* status: 字符串,请求的状态
* isFetching: boolean,是否正在加载
* isLoading: boolean,数据是否第一次加载,调用refetch不会改变状态
* isSuccess: boolean,请求是否成功
* isError: boolean,是否出错
* isUninitialized: boolean,是否还未开始发送
* data: 最新的数据
* currentData: 当前参数的最新数据,和data的区别:当参数发生变化的时候,最开始保存的是undefined
*/
```
### useQuery传参
```js
// useQuery可以接收一个对象作为第二个参数,对请求进行配置
const { isFetching, isSuccess,isError, data } = useGetStuInfoByIdQuery(props.id, {
selectFromResult: result => result, // 对返回进行处理
pollingInterval: 0, // 轮寻间隔,单位毫秒.0表示不轮寻
skip:!props.id, // 是否跳过,默认false
refetchOnMountOrArgChange: false, // 设置是否每次组件挂载都重新加载数据,也可以传时间,单位是秒.例如设置2,表示两秒内,不重新加载
refetchOnFocus: false, // 页面获取焦点时是否重新加载数据,例如切换浏览器标签
refetchOnReconnect: false, // 网络恢复连接重新加载数据
// refetchOnFocus和refetchOnReconnect 需要额外在'@/store/index.js'里添加
// 1. import { setupListeners } from '@reduxjs/toolkit/query'
// 2. setupListeners(store.dispatch)
})
```
### useMutation
#### 1 . 创建模块
```js
// build.mutation 更新的方法
// return的是一个对象,包括url, method, body
// 导出钩子同样有规则, use前缀表示钩子, Mutation表示更新
```
```js
// src/store/stuApi.js
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
export const stuApi = createApi({
reducerPath: 'stuApi',
baseQuery: fetchBaseQuery({ baseUrl: 'http://localhost:1337/api/' }),
endpoints: build => ({
// 删除学生信息
delStuById: build.mutation({
query: id => {
// 如果发送的不是get请求,要返回一个对象,并配置信息
return {
url: `students/${id}`,
method: 'delete',
}
},
}),
// 添加学生
addStu: build.mutation({
query: stu => {
// post请求包含body属性
return {
url: 'students',
method: 'post',
body: { data: stu },
}
},
}),
// 更新学生信息
updateStuById: build.mutation({
query: ({ id, stu }) => {
return {
url: `students/${id}`,
method: 'put',
body: { data: stu },
}
},
}),
}),
})
export const {
useDelStuByIdMutation,
useAddStuMutation,
useUpdateStuByIdMutation,
} = stuApi
```
#### 2 . 配置 store
```jsx
import { configureStore } from '@reduxjs/toolkit'
import { setupListeners } from '@reduxjs/toolkit/query'
import { stuApi } from './stuApi'
// 添加到store
const store = configureStore({
reducer: {
// api对象的路径名当store的标识
// api对象的reducer自动生成的
[stuApi.reducerPath]: stuApi.reducer,
},
// 添加中间件用来实现缓存, 失效, 轮寻
// 必须添加
// 多个中间件, contat(a,b,c)
middleware: getDefaultMiddleware => {
return getDefaultMiddleware().concat(stuApi.middleware)
},
})
setupListeners(store.dispatch)
export default store
```
#### 3 . 注入 store
```jsx
// src/main.jsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import { Provider } from 'react-redux'
import App from './App'
import './index.css'
import store from './store'
ReactDOM.createRoot(document.getElementById('root')).render(
<Provider store={store}>
<App />
</Provider>
)
```
#### 4 . 使用 api
```js
// useMutation和useQuery不一样,返回的是数组,第一个是触发器,第二个是结果
// result的结构类似useQuery返回
// 触发器返回的res也是一个 promise对象
```
```jsx
import {useDelStuByIdMutation} from '@/store/stuApi'
// ...
const [delStu, {isLoading:delLoading}] = useDelStuByIdMutation()
const delStuHandler = () => {delStu(id)} // id是从props中解构得到的
// ...
```
### useMutation 返回的 result对象
```js
/**
* isError: boolean
* isLoading: boolean
* isSuccess: boolean
* isUninitialized: boolean
* originalArgs:
* reset: fn
* status: string
*/
```
### 使数据失效: 数据标签
> 举例: 更新某个学生的信息, 保存成功后, 要让学生列表的组件重渲染
```js
// 1. 设置有哪些标签类型 tagTypes
// 2. 需要更新的api功能设置 providesTags 提供标签
// 3. 触法更新的api功能设置 invalidatesTags 使标签失效
```
```js
// src/store/stuApi.js
/**
* 1. getStuList的标签是'stuList', addStu可以让所有的'stuList'标签失效,从而触发getStuList重新加载数据, 组件重渲染
* 标签可以有多个
* 2. 需要更颗粒化控制, 例如id为1的学生更新了信息, 只需要下次查询id为1的学生不使用缓存, 其他id如果有缓存就使用缓存
* providesTags属性设置为函数,函数返回一个数组,数组里放对象,对象包括type属性和id,id来自query传参的id
* invalidatesTags同样属性设置为函数,函数返回一个数组,数组里放对象,对象包括type属性和id,id来自query传参的id
**/
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
export const stuApi = createApi({
reducerPath: 'stuApi',
baseQuery: fetchBaseQuery({ baseUrl: 'http://localhost:1337/api/' }),
tagTypes: ['stuList','stuInfo'], // 指定api中的标签类型--type
endpoints: build => ({
// 获取学生列表
getStuList: build.query({
query: () => 'students',
// transformResponse对响应数据进行处理
transformResponse(res) {
return res.data
},
providesTags: ['stuList'] // 当标签失效的时候,触发数据加载. 这里的标签可以有多个,其中一个失效,就重新获取
}),
// 根据id获取学生信息
getStuInfoById: build.query({
query: id => `students/${id}`,
transformResponse(res) {
return res.data.attributes
},
keepUnusedDataFor: 30, // 设置缓存时间,单位秒,0是不缓存
providesTags: (result,error,id)=>{
// 设置tag更加详细,可以让某个type+id的失效,而不是整个type
// result: 请求返回的信息
// error: 错误信息
// id: query接收的id
return [{type: 'stuInfo',id}]
}
}),
// 删除学生信息
delStuById: build.mutation({
query: id => {
// 如果发送的不是get请求,要返回一个对象,并配置信息
return {
url: `students/${id}`,
method: 'delete',
}
},
}),
// 添加学生
addStu: build.mutation({
query: stu => {
return {
url: 'students',
method: 'post',
body: { data: stu },
}
},
invalidatesTags: ['stuList'] // 使标签失效
}),
// 更新学生信息
updateStuById: build.mutation({
query: ({ id, stu }) => {
return {
url: `students/${id}`,
method: 'put',
body: { data: stu },
}
},
invalidatesTags: (result,error,id)=>{
return ['stuList',{type:'stuInfo',id}] // 类似providesTags
}
}),
}),
})
export const {
useGetStuListQuery,
useGetStuInfoByIdQuery,
useDelStuByIdMutation,
useAddStuMutation,
useUpdateStuByIdMutation,
} = stuApi
```
上面的`tagTypes`可以简化成一个, 学生列表没有id, 可以给它指定一个独有的id, 如`[{type:'student',id:'list'}]`
```js
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
export const stuApi = createApi({
reducerPath: 'stuApi',
baseQuery: fetchBaseQuery({ baseUrl: 'http://localhost:1337/api/' }),
tagTypes: ['student'], // type改成只有一个
endpoints: build => ({
getStuList: build.query({
query: () => 'students',
transformResponse(res) {
return res.data
},
providesTags: [{type:'student',id:'list'}] // 指定id为'list'
}),
// 根据id获取学生信息
getStuInfoById: build.query({
query: id => `students/${id}`,
transformResponse(res) {
return res.data.attributes
},
keepUnusedDataFor: 30,
providesTags: (result,error,id)=>{
return [{type: 'student',id}]
}
}),
// 删除学生信息
delStuById: build.mutation({
query: id => {
return {
url: `students/${id}`,
method: 'delete',
}
},
invalidatesTags: [{type:'student',id:'list'}] // 使{type:'student',id:'list'}标签失效
}),
// 添加学生
addStu: build.mutation({
query: stu => {
return {
url: 'students',
method: 'post',
body: { data: stu },
}
},
invalidatesTags: [{type:'student',id:'list'}] // 使标签失效
}),
// 更新学生信息
updateStuById: build.mutation({
query: ({ id, stu }) => {
return {
url: `students/${id}`,
method: 'put',
body: { data: stu },
}
},
invalidatesTags: (result,error,id)=>{
return [{type:'student',id:'list'},{type:'student',id}] // 使多个标签失效
}
}),
}),
})
export const {
useGetStuListQuery,
useGetStuInfoByIdQuery,
useDelStuByIdMutation,
useAddStuMutation,
useUpdateStuByIdMutation,
} = stuApi
```
### 设置请求头
```js
// baseQuery 参数里, 除了 baseUrl,还有prepareHeaders用来设置请求头
// 第一个参数是默认headers, 第二个参数有获取state的方法
// 因为useSelect只能在函数组件或者自定义勾子中使用
```
```js
baseQuery: fetchBaseQuery({
baseUrl: 'http://localhost:1337/api/',
prepareHeaders: (headers, { getState }) => {
// 获取token
const token = getState().auth.token
if (token) {
headers.set('Authorization', `Bearer ${token}`)
}
return headers
},
}),
```