redux基础
还记得好久好久之前就想要实现的一个功能吗?
收起侧边栏折叠菜单,没错,现在才实现
因为不是父子通信,所以处理起来相对麻烦一点
可以使用状态树或者中间人模式
这就需要会redux了
Redux工作流:
异步就是比同步多一个中间件
使用它有三大原则:
1.单一数据源
2.State是只读的
3.使用纯函数执行修改
首先安装一下
npm i --save redux react-redux
创建store
创建一个纯函数:CollApsedReducer
// 纯函数
export const CollApsedReducer = (prevState={isCollapsed:false
},action)=>{return prevState
}
//导入antd
import { Layout, theme, Button,Menu } from 'antd'
import { MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons'
import React, { useState } from 'react'
import { changeConfirmLocale } from 'antd/es/modal/locale'
import { Dropdown, Space } from 'antd'
import { DownOutlined, SmileOutlined } from '@ant-design/icons'
import { Avatar } from 'antd'
import { UserOutlined } from '@ant-design/icons'
import { useNavigate } from 'react-router-dom'
import { connect } from 'react-redux'//从Layout组件中解构Header组件
const { Header } = Layout
function TopHeader(props) {console.log(props)//v6的写法
const navigate = useNavigate()const [collapsed, setCollapsed] = useState(false)//定义changeCollapsed函数,用于展开/收起侧边栏,通过取反实现const changeCollapsed = () => {setCollapsed(!collapsed)}const { token } = theme.useToken() // 获取主题 tokenconst { colorBgContainer, borderRadiusLG } = token//使用户名动态渲染// const {role:{roleName},username} = JSON.parse(localStorage.getItem('token'))//使用户名动态渲染const {role:{roleName},username} = JSON.parse(localStorage.getItem('token')) || {}; // 确保 tokenData 是一个对象 const items = [{key: '1',label: (<atarget="_blank"rel="noopener noreferrer"href="https://www.antgroup.com">帮{roleName}做模电实验</a>),},{key: '2',label: (<atarget="_blank"rel="noopener noreferrer"href="https://www.aliyun.com">帮{roleName}上电磁场课</a>),},{key: '3',label: (<atarget="_blank"rel="noopener noreferrer"href="https://www.luohanacademy.com">帮{roleName}辅助面试</a>),},{key: '4',danger: true,label: '要退出吗',onClick: () => {localStorage.removeItem('token')//使用navigate实现重定向navigate('/login')},},]return (<Headerstyle={{padding: '0 16px',background: colorBgContainer,}}><div style={{ float: 'right' }}>{/* 定义欢迎语 */}<span>欢迎<span style={{color:'blue'}}>{username}</span>回来</span>{/* 定义下拉菜单 */}<Dropdownmenu={{items,}}placement="bottomLeft"arrow><Space size={16} wrap><Avatar src={'/头像.jpg'} /></Space>{/* <Button>🥺</Button> */}</Dropdown><Dropdownmenu={{items,}}placement="bottom"arrow></Dropdown></div><Buttontype="text"//展开/收起侧边栏,绑定onClick事件icon={collapsed ? (<MenuUnfoldOutlined onClick={changeCollapsed} />) : (<MenuFoldOutlined onClick={changeCollapsed} />)}onClick={() => setCollapsed(!collapsed)}style={{fontSize: '16px',width: 64,height: 64,}}/></Header>)
}const mapStateToProps = ({CollApsedReducer:{isCollapsed}})=>{return{isCollapsed}
}export default connect(mapStateToProps)(TopHeader)
在这里取出状态
App.jsx:
import { RouterProvider} from 'react-router-dom';
import router from './router/indexRouter';
import './App.css'
import { Provider } from 'react-redux';
import store from './redux/store';function App() {return <Provider store={store}><RouterProvider router={router} />;</Provider>
}export default App;
store.jsx:
import {createStore,combineReducers} from 'redux'
import { CollApsedReducer } from './reducers/CollapsedReducer'const reducer = combineReducers({CollApsedReducer
})
const store = createStore(reducer)export default store
不过现在的createStore已经弃用了
折叠侧边栏
首先开局的时候科文老师的写法就已经弃用了,问了鸡皮替
createStore
已经被官方标记为不推荐使用,在新版 Redux 中推荐使用的是 configureStore
来自 Redux Toolkit(RTK)。
先要安装一下:
npm install @reduxjs/toolkit react-redux
对store.jsx进行重写:
import { configureStore } from '@reduxjs/toolkit'
import CollApsedReducer from './reducers/CollapsedReducer' const store = configureStore({reducer: {CollApsedReducer, // 自动组合多个 reducer},
})export default store
reducer(进行redux state的设计,维护了一个布尔值isCollapsed,reducer函数是纯函数可以根据传入的旧的state和action返回新的state,redux会自动把dispatch的action扔进这个函数里):
const initialState = {isCollapsed: false,
}export default function CollApsedReducer(state = initialState, action) {console.log('CollApsedReducer收到 action:', action)switch (action.type) {case 'change_collapsed':return {...state,isCollapsed: !state.isCollapsed,}default:return state}
}
侧边栏:
import React, { useState, useEffect } from'react';
import { Layout, Menu } from 'antd';
import { useNavigate } from'react-router-dom';
import axios from 'axios';
import {UserOutlined,SettingOutlined,UploadOutlined,VideoCameraOutlined,AuditOutlined,FormOutlined,HomeOutlined,
} from '@ant-design/icons';
import { connect } from 'react-redux';
import './index.css';
import { useLocation } from'react-router-dom';const { SubMenu } = Menu;
const { Sider } = Layout;// **手动映射菜单项对应的图标**
const iconMap = {首页: <HomeOutlined />,用户管理: <UserOutlined />,用户列表: <UserOutlined />,权限管理: <SettingOutlined />,新闻管理: <FormOutlined />,审核管理: <AuditOutlined />,发布管理: <UploadOutlined />,
};function SideMenu(props) {const [menu, setMenu] = useState([]);const location = useLocation(); // 获取当前的路径useEffect(() => {axios.get('http://localhost:3000/rights?_embed=children').then((res) => {setMenu(res.data);}).catch((error) => {console.error('获取菜单数据失败:', error);// 可根据情况设置默认菜单数据或提示用户});
}, []);const navigate = useNavigate();const tokenData = JSON.parse(localStorage.getItem('token')) || {};
const { role = {} } = tokenData;
let allRights = [];// 兼容数组结构(普通角色)和对象结构(超级管理员)
if (Array.isArray(role.rights)) {allRights = role.rights;
} else if (typeof role.rights === 'object' && role.rights !== null) {const { checked = [], halfChecked = [] } = role.rights;allRights = [...checked, ...halfChecked];
}const checkPermission = (item) => {// 检查用户是否具有访问权限return item.pagepermisson && allRights.includes(item.key);};const renderMenu = (menuList) => {return menuList.map((item) => {const icon = iconMap[item.title] || <VideoCameraOutlined />; // 默认图标if (item.children?.length > 0 && checkPermission(item)) {return (<SubMenu key={item.key} icon={icon} title={item.title}>{renderMenu(item.children)}</SubMenu>);}return (checkPermission(item) && (<Menu.Itemkey={item.key}icon={icon}onClick={() => navigate(item.key)}>{item.title}</Menu.Item>));});};//找到路径const selectKeys = [location.pathname];//分割字符串const openKeys = ['/' + location.pathname.split('/')[1]];return (<Sider trigger={null} collapsible collapsed={props.isCollapsed}><div style={{ display: 'flex', height: '100%', flexDirection: 'column' }}><div className="logo">新闻发布系统</div><div style={{ flex: 1, overflow: 'auto' }}><Menutheme="dark"mode="inline"selectedKeys={selectKeys}defaultOpenKeys={openKeys}>{renderMenu(menu)}</Menu></div></div></Sider>);
}const mapStateToProps = ({CollApsedReducer:{isCollapsed}})=>({isCollapsed})export default connect(mapStateToProps)(SideMenu);
//导入antd
import { Layout, theme, Button,Menu } from 'antd'
import { MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons'
import React, { useState } from 'react'
import { changeConfirmLocale } from 'antd/es/modal/locale'
import { Dropdown, Space } from 'antd'
import { DownOutlined, SmileOutlined } from '@ant-design/icons'
import { Avatar } from 'antd'
import { UserOutlined } from '@ant-design/icons'
import { useNavigate } from 'react-router-dom'
import { connect } from 'react-redux'//从Layout组件中解构Header组件
const { Header } = Layout
function TopHeader(props) {console.log(props)//v6的写法
const navigate = useNavigate()const [collapsed, setCollapsed] = useState(false)//定义changeCollapsed函数,用于展开/收起侧边栏,通过取反实现const changeCollapsed = () => {// 改变state的isCollapsed的值// setCollapsed(!collapsed)// console.log(props)props.changeCollapsed()}const { token } = theme.useToken() // 获取主题 tokenconst { colorBgContainer, borderRadiusLG } = token//使用户名动态渲染// const {role:{roleName},username} = JSON.parse(localStorage.getItem('token'))//使用户名动态渲染const {role:{roleName},username} = JSON.parse(localStorage.getItem('token')) || {}; // 确保 tokenData 是一个对象 const items = [{key: '1',label: (<atarget="_blank"rel="noopener noreferrer"href="https://www.antgroup.com">帮{roleName}做模电实验</a>),},{key: '2',label: (<atarget="_blank"rel="noopener noreferrer"href="https://www.aliyun.com">帮{roleName}上电磁场课</a>),},{key: '3',label: (<atarget="_blank"rel="noopener noreferrer"href="https://www.luohanacademy.com">帮{roleName}辅助面试</a>),},{key: '4',danger: true,label: '要退出吗',onClick: () => {localStorage.removeItem('token')//使用navigate实现重定向navigate('/login')},},]return (<Headerstyle={{padding: '0 16px',background: colorBgContainer,}}><div style={{ float: 'right' }}>{/* 定义欢迎语 */}<span>欢迎<span style={{color:'blue'}}>{username}</span>回来</span>{/* 定义下拉菜单 */}<Dropdownmenu={{items,}}placement="bottomLeft"arrow><Space size={16} wrap><Avatar src={'/头像.jpg'} /></Space>{/* <Button>🥺</Button> */}</Dropdown><Dropdownmenu={{items,}}placement="bottom"arrow></Dropdown></div><Buttontype="text"//展开/收起侧边栏,绑定onClick事件icon={props.isCollapsed ? (<MenuUnfoldOutlined onClick={changeCollapsed} />) : (<MenuFoldOutlined onClick={changeCollapsed} />)}onClick={() => setCollapsed(!collapsed)}style={{fontSize: '16px',width: 64,height: 64,}}/></Header>)
}const mapStateToProps = ({CollApsedReducer:{isCollapsed}})=>{return{isCollapsed}
}const mapDispatchToProps ={changeCollapsed(){return{type:"change_collapsed"}}
}export default connect(mapStateToProps,mapDispatchToProps)(TopHeader)
在App.jsx中还要包一下
import { RouterProvider} from 'react-router-dom';
import router from './router/indexRouter';
import './App.css'
import { Provider } from 'react-redux';
import store from './redux/store';function App() {return <Provider store={store}><RouterProvider router={router} />;</Provider>
}export default App;
使用 connect()
获取 Redux 中的 isCollapsed
状态
使用Redux管控侧边栏状态的意义:
loading加载
现在想给程序加上一个加载的效果做指引
加载中 Spin - Ant Designhttps://ant-design.antgroup.com/components/spin-cn这个的使用就是在数据请求的外面包上loading就好了
这个效果应该是让数据请求到了就消失
使用一下拦截器捏!
axios/axios: Promise based HTTP client for the browser and node.jshttps://github.com/axios/axios?tab=readme-ov-file#interceptors
http.jsx:
import axios from "axios";
import store from "../redux/store";
// 对axios做全局配置
axios.defaults.baseURL = "http://localhost:300"// const instance = axios.create();// Add a request interceptor
axios.interceptors.request.use(function (config) {// Do something before request is sent// 显示loadingstore.dispatch({type:"change_loading",payload:true})return config;}, function (error) {// Do something with request errorreturn Promise.reject(error);});// Add a response interceptor
axios.interceptors.response.use(function (response) {// Any status code that lie within the range of 2xx cause this function to trigger// Do something with response data// 隐藏loadingstore.dispatch({type:"change_loading",payload:false})return response;}, function (error) {store.dispatch({type:"change_loading",payload:false})// Any status codes that falls outside the range of 2xx cause this function to trigger// Do something with response errorreturn Promise.reject(error);});
store.jsx:
import { configureStore } from '@reduxjs/toolkit'
import CollApsedReducer from './reducers/CollapsedReducer'
import LoadingReducer from './reducers/LoadingReducer'const store = configureStore({reducer: {CollApsedReducer, // 自动组合多个 reducerLoadingReducer},
})export default store
Loading.jsx:
const initialState = {isLoading: false,}export default function LoadingReducer(state = initialState, action) {// console.log('CollApsedReducer收到 action:', action)let {type,payload} = actionswitch (action.type) {case 'change_loading':return {...state,isLoading: payload}default:return state}}
NewsRouter.jsx:
import SideMenu from "../../components/sandbox/sidemenu";
import TopHeader from '../../components/sandbox/TopHeader'
import { Routes, Route } from'react-router-dom'
import Home from '../../views/sandbox/home/Home'
import RightList from '../../views/sandbox/right-manage/RightList'
import UserList from '../../views/sandbox/user-manage/UserList'
import RoleList from '../../views/sandbox/right-manage/RoleList'
import { Navigate } from'react-router-dom'
import Nopermission from '../../views/sandbox/nopermission/Nopermission'
//引入antd
import { theme, Layout, ConfigProvider, Spin } from 'antd'
import NewsAdd from '../../views/sandbox/news-manage/NewsAdd'
import NewsDraft from '../../views/sandbox/news-manage/NewsDraft'
import NewsCategory from '../../views/sandbox/news-manage/NewsCategory'
import Audit from '../../views/sandbox/audit-manage/Audit'
import AuditList from '../../views/sandbox/audit-manage/AuditList'
import Published from '../../views/sandbox/publish-manage/Published'
import Unpublished from '../../views/sandbox/publish-manage/Unpublished'
import Sunset from '../../views/sandbox/publish-manage/Sunset'
import NewsUpdate from '../../views/sandbox/news-manage/NewsUpdate'
import NewsPreview from '../../views/sandbox/news-manage/NewsPreview'
import { useEffect, useState } from'react'
import axios from 'axios'
import { connect } from "react-redux";//创建一个本地的路由映射表
const LocalRouterMap = {'/home': Home,'/user-manage/list': UserList,'/right-manage/right/list': RightList,'/right-manage/role/list': RoleList,//写什么新闻列表啊,各种权限啊'/news-manage/add': NewsAdd,'/news-manage/draft': NewsDraft,'/news-manage/category': NewsCategory,'/news-manage/preview/:id':NewsPreview,'/news-manage/update/:id':NewsUpdate,'/audit-manage/audit': Audit,'/audit-manage/list': AuditList,'/publish-manage/published': Published,'/publish-manage/unpublished': Unpublished,'/publish-manage/sunset': Sunset,
}function NewsRouter(props) {// 后端返回的路由映射表const [BackRouteList, setBackRouteList] = useState([])useEffect(() => {Promise.all([axios.get('http://localhost:3000/rights'),axios.get('http://localhost:3000/children'),]).then((res) => {setBackRouteList([...res[0].data, ...res[1].data])})}, [])const tokenData = JSON.parse(localStorage.getItem('token')) || {}; // 确保 tokenData 是一个对象const { role = {} } = tokenData; // 确保 role 是一个对象let rights = []; // 初始化 rights 为一个空数组if (role.rights) {if (Array.isArray(role.rights)) {rights = role.rights;} else if (typeof role.rights === 'object') {// 如果 rights 是对象,提取 checked 和 halfChecked 数组并合并rights = [...(role.rights.checked || []), ...(role.rights.halfChecked || [])];}}// console.log(rights)const checkRoute = (item) => {return LocalRouterMap[item.key] && (item.pagepermisson || item.routepermisson)}const checkUserPermisson = (item) => {const hasPermission = rights.includes(item.key);return hasPermission; }return (<Spin size="large" spinning={props.isLoading}><Routes>{/* 动态渲染路由 */}{BackRouteList.map((item) => {const Component = LocalRouterMap[item.key]// console.log(item.key)if (checkRoute(item) && checkUserPermisson(item)) {return (Component && (<Route path={item.key} key={item.key} element={<Component />} />))}return null})}{/* 首页重定向 */}<Route path="/" element={<Navigate to="/home" />} />{/* 权限不足页面 */}{BackRouteList.length > 0 && (<Route path="*" element={<Nopermission />} />)}</Routes></Spin>)
}const mapStateToProps = ({LoadingReducer:{isLoading}})=>({isLoading
})export default connect(mapStateToProps)(NewsRouter)
这样就实现了一闪而过的加载效果勒
持久化
本来我们设置了侧边栏,比如我们把侧边栏收起,但是一刷新就会被打回原形,这就涉及到了持久化的概念
我们应该让redux持久化的存储在系统中捏
redux持久化的工具:
rt2zz/redux-persist: persist and rehydrate a redux storehttps://github.com/rt2zz/redux-persist进行数据持久化的操作:
import { configureStore } from '@reduxjs/toolkit'
import CollApsedReducer from './reducers/CollapsedReducer'
import LoadingReducer from './reducers/LoadingReducer'import { combineReducers} from 'redux'
import { persistStore, persistReducer } from 'redux-persist'
import storage from 'redux-persist/lib/storage' // defaults to localStorage for webconst persistConfig = {key: 'root',storage: storage,blacklist: ['LoadingReducer']//放在黑名单中的数据不会被持久化
}const reducer = combineReducers({CollApsedReducer,LoadingReducer
})const persistedReducer = persistReducer(persistConfig, reducer)const store = configureStore({reducer:persistedReducer
})
const persistor = persistStore(store)export {store,persistor}
持久化有黑名单白名单的机制,放在黑名单的数据就不会被持久化了
持久化数据的逻辑是,配置redux-persist 的规则,然后进行把两个小模块的状态进行合并,最后使用persistReducer包装(persistReducer会根据persistConfig给reducer加上存到localStorage的能力)然后是创建store
最后创建persistor,负责在应用初始化的时候把localStorage里面的数据还原到Redux store中