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中