react后台项目
项目技术选型
react + react-router + react-redux + react-tookit + ts + antd-pro + axios + sass
react:框架
react-router:路由
react-redux/react-tookit:仓库(真实项目中只会用一个)
ts:TypeScript
antd-pro:UI框架
axios:请求
sass:CSS预处理
创建项目
react
+ts
npx create-react-app my-app --template typescript
项目安装sass
npm install sass sass-loader
处理全局样式
1.高度100%
2.字体大小(项目规定的)
3.主题色
4.公共的布局
实现:
src>style写全局样式,通过sass进行模块化划分
封装网络请求
- 注意:
- 做开发是有三个服务器(服务器要进行切换):
- 开发的、测试的、线上的
- 做开发是有三个服务器(服务器要进行切换):
- 实现与页面请求一一对应
axios
可以防止攻击
实现:
1.安装axios,指令:npm install axios
2.src>https>index.tsx,创建axios实例,配置公共属性,添加拦截器
路由
-
安装
npm install react-router-dom
-
配置路由
1.根组件中选择路由模式 2.创建路由表组件
Antd组件
-
安装
npm install antd
-
使用
入口文件引入css:import 'antd/dist/reset.css';
封装输入框、按钮组件
-
思路
1.完成基本样式 2.确定动态属性 3.添加时间、组件通信
// 封装input组件
function Zeroinput(props: any) {
let [type, setType] = useState(props.type) // input类型
let [isEmpty, setIsEmpty] = useState(false) // 校验是否为空
let [value, setValue] = useState('') // input表单的值
let [flag, setFlag] = useState(false) // 控制表单是否开启校验
// 隐藏密码
const changehide = () => {
setType('password')
}
// 显示密码
const changeshow = () => {
setType('text')
}
// 明密文切换
const eye = () => {
// 由于ract的更新机制是重新加载组件,所以此处陆毅不使用监听器
if (type == 'text') {
return (<i onClick={() => changehide()} className={'iconfont ' + 'icon-xianshi'}></i>)
} else {
return (<i onClick={() => changeshow()} className={'iconfont ' + 'icon-yincang'}></i>)
}
}
// 监听
useEffect(() => {
if (!value && flag) { // 表单value值为空且已输入过内容
setIsEmpty(true)
} else {
setIsEmpty(false)
}
}, [value])
// 受控组件(事件双向绑定)
const getValue = (e: any) => {
setValue(e.target.value)
setFlag(true) // 此时开启表单非空校验
}
return (
<div className='zeroinput'>
{/* icon图标 */}
{props.icon ? <i className={'iconfont ' + props.icon}></i> : ''}
{/* input */}
<input onInput={(e) => getValue(e)} type={type} placeholder={props.placeholder} value={value} />
{/* 明密文 */}
{props.eye ? eye() : ''}
{/* 校验提示 */}
{isEmpty ? <div className='isEmpty'>不能为空哦~</div> : ''}
</div>
)
}
export default Zeroinput
// 封装按钮组件
function Zerobutton(params: any) {
return (
<div>
<button>{params.children}</button>
</div>
)
}
export default Zerobutton
权限设计
- 页面级权限
基础版本
1.前端通过路由导航守卫处理内部页面和外部页面
2.后端通过内部接口和外部接口
前端需要告诉服务器自己已经登录:在请求头添加标识(token)
-
前端使用路由守卫
注意:自己写的路由守卫hooks放的位置 放在App组件中可能会导致App组件的多次重新创建,可以考虑放在Layout布局页中
// 自定义hooks--导航守卫,判断是否登录 import { useLocation, useNavigate } from "react-router-dom"; // 导入路由表盒路由信息 import { useEffect } from "react"; // 导入react的hooks // 判断是否登录 export function useRouterBeforeEach() { let location = useLocation() let navigate = useNavigate() // 监听路由的变化 useEffect(() => { console.log('路由信息', location); // 判断内外部页面 if (location.pathname != '/login') { // 内部页面 if (!sessionStorage.getItem('token')) { // 未登录 navigate('/login', { replace: true }) // 无历史记录跳转到登录 } } }, [location]) }
-
前端在请求头添加标识告诉后端自己已登录
// 请求拦截器 server.interceptors.request.use( (config: any) => { //添加token,用来判断内外部接口,告诉服务器自己已登录 if (sessionStorage.getItem('token')) { config.headers['token'] = sessionStorage.getItem('token') } return config }, (err) => { console.log('请求拦截:', err); } )
进阶版本
-
动态生成侧边栏导航
1.根据后端给的数据动态生成侧边栏导航 2.需要会使用 'antd' 的 'Menu'菜单组件的各个属性 3.获取到后端给的数据,使用递归生成侧边栏导航的数据 需要数据结构就可以了
// 侧边导航组件 import React, { useState } from 'react'; // 导入react import { AppstoreOutlined, ContainerOutlined, DesktopOutlined, MailOutlined, MenuFoldOutlined, MenuUnfoldOutlined, PieChartOutlined, } from '@ant-design/icons'; // icon import type { MenuProps } from 'antd'; import { Button, Menu } from 'antd'; type MenuItem = Required<MenuProps>['items'][number]; function getItem( // 每一项 label: React.ReactNode, // 名称 key: React.Key, // key icon?: React.ReactNode, // icon children?: MenuItem[], // 子集 type?: 'group', ): MenuItem { return { key, icon, children, label, type, } as MenuItem; } // 侧边栏数据 => [{}] 数组对象结构 // const items: MenuItem[] = [ // // getItem('名称', 'key', <icon图标 />,[子集]) // getItem('Option 1', '1', <PieChartOutlined />), // getItem('Option 2', '2', <DesktopOutlined />), // getItem('Option 3', '3', <ContainerOutlined />), // getItem('Navigation One', 'sub1', <MailOutlined />, [ // getItem('Option 5', '5'), // getItem('Option 6', '6'), // getItem('Option 7', '7'), // getItem('Option 8', '8'), // ]), // ]; // 递归方法,生成侧边栏数据 function getNavList(list: any, items: any) { // 遍历侧边栏数据list list.forEach((item: any, index: any) => { if (item.children) { // 如果有嵌套,使用递归再次调用此函数 #//第二项(key)后面改成了item.name items.push(getItem(item.name, index, <PieChartOutlined />, getNavList(item.children, []))) } else { // 如果没嵌套 #//第二项(key)后面改成了路由url items.push(getItem(item.name, index, <PieChartOutlined />)) } }) return items } // NavList组件 const NavList: React.FC = () => { const [collapsed, setCollapsed] = useState(false); // 接收后端给的数据 let list = [ { name: "dashboard", children: [{ name: "分析页" }, { name: '监控页', children: [{ name: '张三' }] }] } , { name: '首页' }, { name: '权限设置' } ] // 调用处理侧边栏数据的方法 let items: MenuItem[] = getNavList(list, []) const toggleCollapsed = () => { setCollapsed(!collapsed); }; return ( <div style={{ width: 256 }}> <Menu defaultSelectedKeys={['1']} // 侧边栏选中效果 defaultOpenKeys={['sub1']} // 展开嵌套的子集 mode="inline" // 菜单类型 theme="dark" // 主题颜色 inlineCollapsed={collapsed} // 收缩与展开 items={items} // 侧边栏数据 /> </div> ); }; export default NavList; /* defaultOpenKeys:展开嵌套的子集 defaultSelectedKeys:侧边栏选中效果 inlineCollapsed:收缩与展开 items:菜单内容、侧边栏数据 mode:菜单类型:垂直、水平和内嵌 theme:主题颜色 onClick:点击 MenuItem 触发时间 */
集合版本
-
动态生成路由
路由: 1.写死的路由(公共页面) 2.动态路由 实现步骤: 1.获取到后端的数据 2.将数据扁平化(拍平数组) 3.对数据进行遍历
-
扁平化
// 路由表 // 导入路由表、路由信息 import { Routes, Route } from "react-router-dom"; // 引入组件 import Login from "../views/Login"; import Layout from "../Layout"; // 扁平化:多维数组=>一维数组 function getRoutesList(list: any, arrs: any) { // 遍历后端给的数据 list.forEach((item: any, index: number) => { if (item.children) { // 如果有嵌套,则递归在次调用 getRoutesList(item.children, arrs) } else { // 如果没嵌套,则直接添加 arrs.push(item) } }); return arrs } // 创建路由表组件 function RouterList() { /* 获取到用户登录时给的路由信息(动态路由信息) 注意:后端给到结构是数型结构,我们的导航栏路由信息(扁平化) 不管侧边栏嵌套多少成,只要点击导航右边就是对应的页面, 路由信息多少第一层(这也就是扁平化的原因) */ // 获取到用户登录时后端给的数据 let list = [ { name: "dashboard", children: [{ name: "分析页" }, { name: '监控页' }] }, { name: '首页' }, { name: '权限设置' } ] // 扁平化:将多维数组变成一维数组 let arrs = getRoutesList(list, []) // console.log('路由数据扁平化', arrs); return ( <Routes> <Route path="/login" element={<Login />}></Route> <Route path="/" element={<Layout />}></Route> </Routes> ) } export default RouterList
-
如果后端给的数据没有路径,则需要对路由进行处理。(查表思想)
- 在
utitls
创建一个文件
// 处理路由(查表思想),根据名称返回路径 // 创建一个路由信息表 let routerList = [ { name: "分析页", path: 'fenxi', Com: 'Fenxi' }, { name: "监控页", path: 'jiankong', Com: 'Jiankong' }, { name: "首页", path: 'index', Com: 'Index' }, { name: "权限设置", path: 'limit', Com: 'Limit' } ] // 查表方法 export function findRouteUrl(obj: any) { let ob = routerList.find((item) => { // 查表 return item.name == obj.name }) // 如果查到了则返回路径 if (ob) { return ob.path } else { return 'notfound' } }
- 在
-
处理组件(路由懒加载组件)
// 路由表 // 导入react
import React, { Suspense } from "react"; // 引入处理路由方法 import { findRouteUrl, findRouteCom } from "../utils/routerM"; // 导入路由表、路由信息 import { Routes, Route } from "react-router-dom"; // 引入组件 import Login from "../views/Login"; import Layout from "../Layout"; // 自动创建路由懒加载组件 function getLazyCom(obj: any) { let Com Com = React.lazy(() => import(
../views/${findRouteCom(obj)}/index
)) // 返回懒加载组件 return } // 扁平化:多维数组=>一维数组 function getRoutesList(list: any, arrs: any) { // 遍历后端给的数据 list.forEach((item: any, index: number) => { if (item.children) { // 如果有嵌套,则递归在次调用 getRoutesList(item.children, arrs) } else { // 如果没嵌套,则直接添加 arrs.push(item) } }); return arrs } // 创建路由表组件 function RouterList() { /* 获取到用户登录时给的路由信息(动态路由信息) 注意:后端给到结构是数型结构,我们的导航栏路由信息(扁平化) 不管侧边栏嵌套多少成,只要点击导航右边就是对应的页面, 路由信息多少第一层(这也就是扁平化的原因) */// 获取到用户登录时后端给的数据 let list = [ { name: "dashboard", children: [{ name: "分析页" }, { name: '监控页' }] }, { name: '首页' }, { name: '权限设置' } ] // 扁平化:将多维数组变成一维数组 let arrs = getRoutesList(list, []) // console.log('路由数据扁平化', arrs); return ( <Suspense fallback={<div>loading......</div>}> <Routes> <Route path="/login" element={<Login />}></Route> <Route path="/" element={<Layout />}> {/* 动态处理路由 */} { arrs.map((item: any, index: number) => { return ( <Route path={findRouteUrl(item)} key={index} element={getLazyCom(item)}></Route> ) }) } </Route> </Routes> </Suspense> )
} export default RouterList
```react // 处理路由组件(查表思想) export function findRouteCom(obj: any) { // 查表 let ob = routerList.find((item: any) => { return item.name == obj.name }) // 查到了则返回该数据对应的组件路径片段 if (ob) { return ob.Com } else { return 'Notfound' } }
-
动态处理icon,查表思想
// 处理icon图标 // 创建一张表 import { AppstoreOutlined, ContainerOutlined, DesktopOutlined, MailOutlined, MenuFoldOutlined, MenuUnfoldOutlined, PieChartOutlined, } from '@ant-design/icons'; //icon let routesIcon: any = [ { name: "dashboard", path: 'fenxi', ComI: <AppstoreOutlined></AppstoreOutlined> }, { name: "分析页", path: 'fenxi', ComI: '' }, { name: "监控页", path: 'jiankong', ComI: '' }, { name: "首页", path: 'index', ComI: <MailOutlined></MailOutlined> }, { name: "权限设置", path: 'limit', ComI: <PieChartOutlined></PieChartOutlined> } ] // 查表 export function findRouteIcon(obj: any) { let ob = routesIcon.find((item: any) => { return item.name == obj.name }) if (ob) { return ob.ComI } else { return '' } }
// NavList组件 ... function getNavList(list: any, items: any) { // 遍历侧边栏数据list list.forEach((item: any, index: any) => { if (item.children) { // 如果有嵌套,使用递归再次调用此函数 items.push(getItem(item.name, item.name, findRouteIcon(item), getNavList(item.children, []))) } else { // 如果没嵌套 items.push(getItem(item.name, findRouteUrl(item), findRouteIcon(item))) } }) return items } ...
-
展开与收缩
<div className='foot-shousuo'> {collapsed ? <i onClick={() => toggleCollapsed()} className='iconfont icon-zhankai'></i> : <i onClick={() => toggleCollapsed()} className='iconfont icon-shousuo'></i>} </div>
-
点击侧边栏,侧边栏内容重新渲染问题
原因:加载效果给了全局 => 布局组件 分析:用户点击侧边栏后跳转对应的组件,加载效果应该给右侧显示的组件
import {Outlet} from 'react-router-dom' import React,{ Suspense} from 'react' function Content(){ return ( <div> <Suspense fallback={<div>...loading</div>}> <Outlet></Outlet> </Suspense> </div> ) } export default Content
-
头部组件
封装搜索组件
-
功能
1.给icon添加点击事件,显示input组件 2.inpit组件,聚焦显示弹框组件 2.选中弹框中的内容,关闭弹框组件,input中有对应内容
// 搜索组件 import "./Zerosearch.scss"; import { useState } from "react"; function Zerosearch() { let [show, setShow] = useState(false) // 控制input是否显示 // 显示input const showIpt = () => { setShow(!show) } return ( <div className="zerosearch"> <div className="icon"> <i className="iconfont icon-sousuo" onClick={() => showIpt()}></i> </div> {show ? <InputSelect /> : ''} </div> ) } // input function InputSelect() { let list = ['umi ui', 'Zero', 'zhang'] // 弹框内容 let [showpop, setShowpop] = useState(false) // 控制弹框是否显示 let [value, setValue] = useState(list[0]) // 打开弹框 const focusI = () => { setShowpop(!showpop) } // 表单赋值 const changeItem = (item: any) => { setValue(item) } // 关闭弹框 const BlurIn = () => { setTimeout(() => { setShowpop(!showpop) }, 100) } return ( <div className="inputBox"> <input type="text" onFocus={() => focusI()} value={value} onBlur={() => BlurIn()} /> {/* 弹框 */} {showpop ? <div className="pop"> { list.map((item, index) => { return ( <div className="popItem" key={index} onClick={() => changeItem(item)}>{item}</div> ) }) } </div> : ''} </div> ) } export default Zerosearch
封装消息组件
-
功能
1.基本样式 2.添加点击事件 => 显示组件 3.列表渲染头部 4.发送请求获取数据 5.显示数据 6.默认显示通知,点击每一项,添加高亮,通知栏同理 7.底部处理 8.头部数据优化 9.用户处理消息,自己修改每一项的数据
// 消息组件 import "./zeromessage.scss";// 样式
import { messageTopData, ChangeMessage } from "../../https/api/layout"; // 请求 import { useEffect, useState } from "react";
function Zeromessage() { let [show, setShow] = useState(false) // 控制是否显示消息弹窗 // 打开消息弹框 const showMessageData = () => { setShow(!show) } return (
) }// 消息弹框组件 function MassageData() { let topList = ['通知', '消息', '代办'] // 头部内容 let [currentIndex, setCurrentIndex] = useState(0) // 默认选中头部第一项 let [messageObj, setMessageObj] = useState({ infrom: [], messageList: [], commissionList: [] }) // 弹窗数据 let [listContent, setListContent] = useState([{ zhutai: false }]) // 列表的数据 // 获取弹窗数据方法 const getmessAgeDataList = () => { let token = sessionStorage.getItem('token') // 发起请求 messageTopData({ token }).then((res: any) => { console.log('弹窗数据', res); setMessageObj(state => { return { ...res.messageObj } }) // 默认数据-通知列表 setListContent(res.messageObj.infrom) }) } // !!在页面加载完毕后,发送请求:防止页面重绘和回流 useEffect(() => { // 获取弹窗数据 getmessAgeDataList() }, []) // 点击头部切换,显示内容 const selectItem = (index: number) => { setCurrentIndex(index) if (index == 0) { setListContent(messageObj.infrom) } else if (index == 1) { setListContent(messageObj.messageList) } else { setListContent(messageObj.commissionList) } } // 前端手动修改已读 const findListData = (index: number) => { setListContent(state => { // 修改已读状态 state[index].zhutai = true return [...state] }) } // 点击每一项,处理已读 const selectCItem = (index: number) => { // 发送请求 ChangeMessage({ indexID: index }).then((res: any) => { if (res.code == 200) { // 修改已读样式 findListData(index) } }) }
return (
<div className="messageData">
{/* 头部 */}
<div className="messageTop">
{
topList.map((item, index) => {
return (
<div className={currentIndex == index ? 'topItem' : ''} key={index} onClick={() => selectItem(index)}>{item}</div>
)
})
}
</div>
{/* 弹窗内容列表 */}
<div className="listContent">
{
listContent.map((item: any, index: number) => {
return (
<div className={item.zhutai ? 'listItem' : ''} onClick={() => selectCItem(index)} key={index}>
<div>{item.content}</div>
<div>{item.time}</div>
</div>
)
})
}
</div>
{/* 尾部 */}
<div className="footMessage">
<div>清空</div>
<div>查看更多</div>
</div>
</div>
)
} export default Zeromessage
### 退出登录组件
- 使用了 `antd` 的 `下拉菜单` 组件
```react
// 头部退出登录组件
import "./zeroOutLogin.scss"; // 样式
import React from 'react';
import { useNavigate } from "react-router-dom";
import type { MenuProps } from 'antd';
import { Button, Dropdown, Space } from 'antd';
// 下拉框中每一项的数据
const items: MenuProps['items'] = [
{ key: '1', label: (<div>个人中心</div>), },
{ key: '2', label: (<div>个人设置</div>), },
{ key: '3', label: (<div>退出登录</div>), },
];
// 组件
function ZeroOutLogin() {
let navigate = useNavigate()
// 获取用户信息
let userObj = JSON.parse(sessionStorage.getItem('user') as any)
// 点击事件
const onClick: MenuProps['onClick'] = (obj: any) => {
// console.log('退出登录组件', obj);
if (obj.key == 3) {
// 清楚本地数据
sessionStorage.clear()
// 跳转登录(无记录跳转)
navigate('/login', { replace: true })
}
}
return (
<div className="zeroOutLogin">
<Dropdown menu={{ items, onClick }} placement="bottomLeft">
<div className="content">
<img src={userObj.image} />
<div>{userObj.name}</div>
</div>
</Dropdown>
</div>
)
};
export default ZeroOutLogin;
封装面包屑组件
- 引用
antd
中的面包屑
组件,二次处理 - 面包屑数据为数组数据结构
- 首次进入后台系统,展示
首页
// 面包屑组件
import "./zeroBreadcrumb.scss"; // 样式
import { useState, useEffect } from 'react';
import { useNavigate, useLocation } from "react-router-dom";
import { Breadcrumb } from 'antd'; // 面包屑组件
// 创建一张面包屑对应的表
let pathNameCode = [
{ path: '/index', name: ['首页'], },
{ path: '/limit', name: ["权限"] },
// 侧边导航,有嵌套
{ path: '/fenxi', name: ['dashboard', '分析'] },
// 侧边导航,有嵌套
{ path: '/jiankong', name: ['dashboard', '监控'] }
]
// 查表方法
function getPathName(path: string) {
// 查表
let item = pathNameCode.find((item) => {
return item.path == path
})
// 如果查到了,则返回面包屑数据
if (item) {
return item.name
}
}
function ZeroBreadcrumb() {
let navigate = useNavigate() // 路由跳转
let location = useLocation() // 路由信息
let [breadList, setBreadList] = useState(['首页']) // 面包屑数据,默认显示首页
// 监听路由信息
useEffect(() => {
console.log('面包屑监听路由信息');
if (location.pathname != '/index') {
// !!查表思想
let arr: any = getPathName(location.pathname)
setBreadList(start => {
return ['首页', ...arr]
})
} else {
// 点击首页,面包屑只展示首页
setBreadList(['首页'])
}
}, [location])
// 点击首页,跳转首页
const goPathIndex = (index: number) => {
if (index == 0) {
navigate('/index')
sessionStorage.setItem('currentPath', 'index')
}
}
return (
// separator => 分隔符
<div className="zeroBreadcrumb">
<Breadcrumb separator='/'>
{
breadList.map((item, index) => {
return (
<Breadcrumb.Item onClick={() => goPathIndex(index)} key={index}>{item}</Breadcrumb.Item>
)
})
}
</Breadcrumb>
</div >
)
}
export default ZeroBreadcrumb
存在问题
-
问题
1.重启项目 => 访问'/'路由去布局组件 => 加载头部的'退出登录组件'时,由于头像使用了用户信息 => 用户信息为空则报错 2.面包屑组件 => 检测到用户进入'/'页面 => 添加面包屑数据 3.用户进入后台 => 默认展示'首页' => 加载侧边栏组件
-
解决
// 退出登录组件 ... // 获取用户信息 // !!解决重启项目用户信息为空的问题 let datas = sessionStorage.getItem('user') || false let userObj = datas ? JSON.parse(datas) : {} ... {/* // !!解决重启项目用户信息为空的问题 */} {datas ? <img src={userObj.image} /> : ''} ...
// 面包屑组件 ... // 监听路由信息 useEffect(() => { console.log('面包屑监听路由信息'); // !!解决重启项目是面包屑问题 if (location.pathname != '/index' && location.pathname != '/') { // !!查表思想 let arr: any = getPathName(location.pathname) setBreadList(start => { return ['首页', ...arr] }) } else { // 点击首页,面包屑只展示首页 setBreadList(['首页']) } }, [location]) ...
路由标签组件
-
思路
1.完成基本样式 2.用户进入后台显示'首页' 3.用户'切换'侧边栏导航时,添加一个路由标签(查表思想:根据路径返回名称) 4.从第二个路由标签开始,显示可删除的icon图标 5.当前路由标签具有高亮效果 6.点击删除路由标签 7.点击路由标签跳转
-
实现
1.切换侧边栏导航添加路由标签,不重复(查表思想:根据路径返回名称) 2.处理高亮效果,当前路径与标签路径一致时高亮显示 3.添加删除的icon图标 4.点击删除 4-1.没有高亮效果的,根据index删除 4-2.有高亮效果的,根据index删除并跳转至路由标签最后一项 5.点击路由标签进行跳转
// 路由标签 import "./routerTags.scss"; import { useState, useEffect } from "react"; import { tagName } from "../../utils/tagName"; //查表思想 import { useLocation, useNavigate } from "react-router-dom"; // 路由信息,路由跳转 function RouterTags() { let location = useLocation() // 路由信息 let navigate = useNavigate() // 路由跳转 let [routerTagsList, setRouterTagsList] = useState([{ name: '首页', path: '/index', active: true, del: false }]) // 路由标签数据,默认展示首页 // 添加路由标签方法 const addRouteTags = (pathname: string) => { // 路由标签中查询一遍,判断该路由标签是否存在 let index = routerTagsList.findIndex((item) => { return item.path == pathname }) // 判断该路由标签是否存在 if (index != -1) { // 该路由标签存在,则高亮显示 changStyle(pathname) } else { // 该路由标签存在,则添加 setRouterTagsList((state: any) => { // concat => 合并数组 let arrs = state.concat({ path: pathname, show: true, del: false, name: tagName(pathname) }) return [...arrs] }) // 处理高亮 changStyle(pathname) } } // 处理高亮显示方法 const changStyle = (pathname: string) => { setRouterTagsList(state => { state.forEach((item: any) => { if (item.path == pathname) { // 当前项高亮 item.active = true } else { // 其余项不高亮 item.active = false } }) return [...state] }) } // 监听路由 useEffect(() => { // 添加路由标签,去重 addRouteTags(location.pathname) }, [location]) // 点击删除 const deleteIndex = (item: any, index: number) => { // 判断该项是否高亮(活动)项 if (item.active) { // 高亮 => 根据index伤处并跳转最后一个路由标签 setRouterTagsList(state => { state.splice(index, 1) // 根据index删除 let url = state[state.length - 1].path // 获取最后一个路由标签的路由 navigate(url) // 跳转 return [...state] }) } else { // 非高亮 => 根据index删除 setRouterTagsList(state => { state.splice(index, 1) // 根据index删除 return [...state] }) } } // 点击路由标签,路由跳转 const goPath = (path: string) => { navigate(path) } // 鼠标移入 const mouseEnter = (name: string) => { setRouterTagsList(state => { state.forEach((item: any, index: number) => { // 该项显示删除的icon图标 if (item.name == name) { item.del = true } else { // 其他项不显示删除的icon图标 item.del = false } }) return [...state] }) } // 鼠标移出 const mouseOut = (name: string) => { // 不显示删除的icon图标 setRouterTagsList(state => { state.forEach((item) => { item.del = false }) return [...state] }) } return ( <div className="routerTags"> { routerTagsList.map((item, index) => { return ( <div key={index} className={item.active ? 'tagItemA' : 'tagItem'} > <span onClick={() => goPath(item.path)} onMouseEnter={() => mouseEnter(item.name)} onMouseOut={() => mouseOut(item.name)}>{item.name}</span> {/* 删除的icon图标 */} {/* 为了解决鼠标在padding空隙中的闪烁问题,在<i/>标签中也添加了移入移出事件 */} { item.name != '首页' && item.del ? <i className="iconfont icon-shanchu" onClick={() => deleteIndex(item, index)} onMouseEnter={() => mouseEnter(item.name)} onMouseOut={() => mouseOut(item.name)}></i> : '' } </div> ) }) } </div> ) } export default RouterTags
// 路由标签组件 查表思想 utils>tagName.tsx // 根据路径返回名称 // 创建一张表 let routerList: any = [ { name: "分析页", path: '/fenxi' }, { name: "监控页", path: '/jiankong' }, { name: "首页", path: '/index' }, { name: "权限设置", path: '/limit' } ] // 查表 export function tagName(path: any) { let ob = routerList.find((item: any) => { return item.path == path }) if (ob) { return ob.name } else { return 'Notfound' } }
全局设置组件
-
思路
1.完成基本样式 1-1.设置按钮放在头部中,点击设置打开弹框 1-2.设置弹框凡在布局组件中,与头部组件同级 2.功能 2-1.组件间数据传递(多个组件需要使用这个数据),使用redux全局状态管理
-
安装
redux
npm install redux npm install react-redux
-
实现
-
src
>stort
>index
创建仓库// 创建仓库 // redux => store actions reducer import { createStore } from "redux"; // 引入redux中的创建仓库方法 import reducers from "./reducers"; // 引入reducer行为 let store = createStore(reducers) // 创建仓库 // 仓库创建完毕,将仓库和项目关联 => react-redux export default store
-
创建各自模块的
reducer
行为,合并reducer
行为scr>store>redcues>模块文件
// 创建reducer行为 // 默认数据 let data = { showSet: false, // 控制设置弹框的显隐 selectLayout: 3 // 布局:1:左右、2:上下、3:上左右 ["左右","上下","上左右"] } // 行为 function glbSetState(state: any = data, actions: any) { switch (actions.type) { // 打开弹窗 case 'showT': return { ...state, showSet: true } // 关闭弹窗 case 'showF': return { ...state, showSet: false } // 设置布局 case 'setLoyout': return { ...state, selectLayout: actions.index } default: return state; } } export default glbSetState
// 合并reducer行为 import { combineReducers } from "redux"; // 引入combineReducers方法 import glbSetState from './glbSetState'; // 引入各个reducer模块 // 合并reducer let reducers = combineReducers({ glbSetState }) export default reducers // 导出合并好的模块
-
使用
react-redux
让仓库和项目关联// 入口文件 index.tsx ... import store from "./store/index"; // 仓库 import { Provider } from "react-redux"; // 仓库和项目关联 ... root.render( <Provider store={store}> <BrowserRouter> <App /> </BrowserRouter> </Provider> );
-
功能1:点击
设置
,打开弹框// 头部组件中 ... // 引入react-redux提供的useDispatch import { useDispatch } from "react-redux"; function LayoutTop() { let dispatch = useDispatch() // dispath // 点击设置,打开弹窗 const showSetPop = () => { // 触发行为 dispatch({ type: 'showT' }) } ... }
-
功能2:关闭弹框
dispath({ type: 'showF' })
-
功能3:切换布局
// 布局切换子组件 function SelectNav() { let dispach = useDispatch() // dispach let list = ['左右布局', '上下布局', '左中右布局'] let setState = useSelector((state: any) => state.glbSetState.selectLayout) // 获取仓库里的布局数据 // 点击切换布局 const selectType = (index: number) => { dispach({ type: 'setLoyout', index: index + 1 }) } return ( <div className="selectNav"> <h2>布局切换</h2> <div className="selectNavList"> { list.map((item: any, index: number) => { return ( <div className={setState - 1 == index ? 'listItemA' : 'listItem'} key={index} onClick={() => selectType(index)}> {item} </div> ) }) } </div> </div> ) }
-
功能2、功能3代码整合
// 弹框组件 import "./setPop.scss"; // 样式 import { useState, useEffect } from 'react'; import { Drawer, Switch } from 'antd'; import { useDispatch, useSelector } from "react-redux"; function SetPop() { let dispach = useDispatch() // dispach // 获取仓库中的数据 let showSet = useSelector((state: any) => state.glbSetState.showSet) // 弹框的显隐 const [open, setOpen] = useState(false); // 监听仓库中showSet的变化 useEffect(() => { setOpen(showSet) }, [showSet]) // 关闭弹窗 const onClose = () => { dispach({ type: 'showF' }) }; return ( <div> <Drawer title="页面设置" placement="right" onClose={onClose} open={open}> {/* 主题切换 */} <SelectTheme /> {/* 布局切换 */} <SelectNav /> </Drawer> </div> ); }; // 主题切换组件 function SelectTheme() { let defaultChecked = false return ( <div className='changeTheme'> <h2>主题</h2> <Switch checkedChildren="☀" unCheckedChildren="🌙" defaultChecked={defaultChecked} /> </div> ) } // 布局切换组件 function SelectNav() { let dispach = useDispatch() // dispach let list = ['左右布局', '上下布局', '左中右布局'] let setState = useSelector((state: any) => state.glbSetState.selectLayout) // 获取仓库里的布局数据 // 点击切换布局 const selectType = (index: number) => { dispach({ type: 'setLoyout', index: index + 1 }) } return ( <div className="selectNav"> <h2>布局切换</h2> <div className="selectNavList"> { list.map((item: any, index: number) => { return ( <div className={setState - 1 == index ? 'listItemA' : 'listItem'} key={index} onClick={() => selectType(index)}> {item} </div> ) }) } </div> </div> ) } export default SetPop;
-
功能4:对应布局展示
// layout布局组件 function Layout() { console.log('layout'); // 获取仓库数据 let showSet = useSelector((state: any) => state.glbSetState.showSet) // 弹窗显隐 let selectLayout = useSelector((state: any) => state.glbSetState.selectLayout) // 布局 useRouterBeforeEach() //路由导航守卫,判断是否登录 // 判断当前布局模式 const getLayout = (num: number) => { if (num == 3) { // 左中右布局 return ( <div className="layout"> {/* 头部 */} <LayoutTop /> <div className="main"> {/* 侧边栏 */} <NavList /> <div className='right'> {/* 路由标签 */} <RouterTags /> {/* 内容显示区域 */} <Content /> </div> </div> </div > ) } else if (num == 2) { // 上下布局 return ( <div className="layout"> {/* 头部 */} <LayoutTop /> <div className="main"> <div className='right'> {/* 路由标签 */} <RouterTags /> {/* 内容显示区域 */} <Content /> </div> </div> </div > ) } else { // 左右布局 return ( <div className="layout"> <div className="main"> {/* 侧边栏 */} <NavList /> <div className='right'> {/* 头部 */} <LayoutTop /> {/* 路由标签 */} <RouterTags /> {/* 内容显示区域 */} <Content /> </div> </div> </div > ) } } return ( <div className='layout'> {getLayout(selectLayout)} {/* 弹框组件 */} {showSet ? <SetPop /> : ''} </div> ) } export default Layout
-
首页(ECharts)
-
安装
ECharts
npm install echarts --save
-
对常有用的可视化图表进行封装的优点
1.统一标准 2.提高开发效率 3.方便二次维护
-
实现步骤
根据ui图 >> 找类似的示例 >> 找对应的属性 >> 死数据 >> 和ui图样式一致
-
ECharts
的使用1.导入 2.生成ECharts图表 2-1.获取元素,用来放置图表 let dom: any = document.getElementById('main') 2-2.创建echarts实例 let myChart = echarts.init(dom) 2-3.配置项 myChart.setOption({ xAxis:{}, yAxis:{}, series:{} })
-
实现(以下代码处于一个文件)
// 首页-引入 import "./index.scss"; import * as echarts from 'echarts'; // 引入ECharts import { DatePicker } from 'antd'; // 日期选择器(方法) import { useState, useEffect } from "react"; import { getIndexFenxi, getIndexEcharts,getIndexEchartsTwo } from "../../https/api/index"; // 引入请求
// 首页组件(父组件) function Index() { let [topshow, settopshow] = useState([]) // 获取会员分析数据 let [echartsD, setEchartsD] = useState({}) // 获取会员分析ECharts数据 let [echartsTwo, setEchartsTwo] = useState({}) // 会员等级(多个系列)图表 // 挂载后生命周期 useEffect(() => { // 发起请求,获取数据 getshowData() getIndexEchartsd() getTwoEcharts() }, []) // 获取会员分析数据(父组件发起请求) const getshowData = (time = '最近7天', endTime = '') => { // 发起请求 getIndexFenxi({ time, endTime }).then((res: any) => { // 获取到数据 settopshow(res.rember) }) } // 获取会员分析ECharts数据(父组件发起请求) const getIndexEchartsd = (time = '最近7天', endTime = '') => { // 发起请求 getIndexEcharts({ time, endTime }).then((res: any) => { // 获取到会员分析ECharts数据 setEchartsD(res.echartsObj) }) } // 获取会员等级(多个系列)的ECharts数据 const getTwoEcharts = (time = '最近7天', endTime = "") => { // 发起请求 getIndexEchartsTwo({ time, endTime }).then((res: any) => { // 获取到会员等级(多个系列)的ECharts数据 setEchartsTwo(res.echartsObj) }) } // 通过判断,获取会员分析数据与ECharts数据(父组件发起请求) const getTopSt = (val: any, enTtime: any) => { // console.log('获取会员数据和ECharts数据:', val, enTtime); if (enTtime) { // 表示为日期选择器选择了日期 // 发起请求,获取会员分析数据 getshowData(val, enTtime) } else { // 表示时间选项选择了数据 getshowData(val) // 发起请求,获取会员分析数据 getIndexEchartsd(val) // 发起请求,获取会员分析ECharts数据 getTwoEcharts(val) // 发起请求,获取到会员等级(多个系列)的ECharts数据 } } return ( <div className="indexEcharts"> <div className="index-top"> <div><b>交易分析</b></div> {/* 日期选择 */} <SelectTime getTopSt={getTopSt} /> </div> {/* 会员分析模块 */} <Meber topshow={topshow} /> {/* 会员数据图表展示 */} <Secharts echartsD={echartsD} /> {/* 会员等级占比优势图标展示2 */} <SechartsTwo echartsTwo={echartsTwo} /> </div> ) }
// 日期选择组件(子组件) function SelectTime(props: any) { let { RangePicker } = DatePicker // 日期选择器(组件) let timeList = ['今天', '昨天', '最近7天', '最近30天'] // 选项数据 let [topSelect, setTopSelect] = useState('最近7天') // 控制选项高亮 let [selectLeft, setSelectLeft] = useState(false) // 日期选择器是否选择了时间的状态 // 点击切换选项 const selectTop = (item: string) => { props.getTopSt(item) // 子传父,触发父组件的判断方法,发起请求 setTopSelect(item) // 设置高亮 setSelectLeft(false) // 更改日期选择器是否选择了时间的状态 } // 日期选择器发生改变 const SelectTimeS = (e: any) => { console.log('日期选择器', e); if (e) { // e[0].$d:起始日期 e[1].$d:结束日期 props.getTopSt(e[0].$d, e[1].$d) // 子传父,触发父组件的判断方法,发起请求 } setSelectLeft(true) // 更改日期选择器是否选择了时间的状态 } return ( <div className='top-right'> <div className="right-time"> 统计日期: <RangePicker onChange={(e) => SelectTimeS(e)} /> </div> <div className="right-select"> { timeList.map((item, index) => { return ( <div key={index} className={topSelect == item && !selectLeft ? 'selectItemA' : ''} onClick={() => selectTop(item)}> {item} </div> ) }) } </div> </div> ) }
// 会员分析模块组件(子组件) function Meber(props: any) { // console.log('会员分析', props); // 打印2次 => 原理:组件创建 比 异步获取数据 快 // 显示数据的标题 const showRemberTitle = (index: number) => { if (index == 0) { return '累计会员数' } else if (index == 1) { return '新增会员数' } else if (index == 2) { return '支付会员数' } else { return '储值会员数' } } return ( <div className="meberShow"> { props.topshow.map((item: any, index: number) => { return ( <div key={index} className='meberItem'> {/* 标题 */} <div> {showRemberTitle(index)} </div> {/* 数据 */} <div className="meberdata"> {item.all || item.newR || item.paym || item.sm} </div> {/* 比较 */} <div> 较上期 <i className="iconfont icon-xiangshang">{item.allb || item.newRb || item.paymb || item.smb}</i> </div> </div> ) }) } </div> ) }
// 会员等级占比优势模块(多个系列的图表)(子组件) function SechartsTwo(props: any) { console.log('会员等级图标展示', props); // 打印2次 => 原理:组件创建 比 异步获取数据 快 // 监听数据变化 useEffect(() => { if (props.echartsTwo.ydata) { getECharts() } }, [props]) // 生成ECharts图表 const getECharts = () => { let dom: any = document.getElementById('mains') // 获取元素,用来放置图表 let myChart = echarts.init(dom) // 创建echarts实例 // 配置项 => 绘制图表 myChart.setOption({ // 提示框组件 tooltip: { trigger: 'axis' // 触发类型 }, // 配置X轴 xAxis: { type: 'category', // 坐标轴类型:如:类目轴 boundaryGap: false, // 坐标轴两边的留白策略 data: props.echartsTwo.xdata // 类目数据 }, // 配置Y轴 yAxis: { type: 'value' // 坐标轴类型:如:数值轴 }, // 图表 series: [ { data: props.echartsTwo.ydata.oneRe, // 图表数据 type: 'line', // 图标类型:如:折线/面积图 name: '白金会员', // 系列名称 symbol: 'none', // 标记的图形 stack: 'Total', // 数据的堆叠 }, { data: props.echartsTwo.ydata.twoRe, // 图表数据 type: 'line', // 图标类型:如:折线/面积图 name: '黄金会员', // 系列名称 symbol: 'none', // 标记的图形 stack: 'Total', // 数据的堆叠 }, { data: props.echartsTwo.ydata.threeRe, // 图表数据 type: 'line', // 图标类型:如:折线/面积图 name: '钛金会员', // 系列名称 symbol: 'none', // 标记的图形 stack: 'Total', // 数据的堆叠 }, { data: props.echartsTwo.ydata.foreRe, // 图表数据 type: 'line', // 图标类型:如:折线/面积图 name: '超级会员', // 系列名称 symbol: 'none', // 标记的图形 stack: 'Total', // 数据的堆叠 }, ] }) } return ( <div className="echartsBoxTwo"> <div> <div>会员等级占比趋势</div> </div> <div id="mains" style={{ width: 100 + '%', height: 350 + 'px' }}></div> </div> ) }
export default Index // 导出首页组件(父组件)
-
**注意:**如果在同一个页面级组件中展示多个
ECharts
,dom
元素的id
不能相同
分析页(二次封装ECharts)
-
本质
1.组件的数据传递 2.确定动态属性(函数劫持),动态属性越多,复用性越强
-
封装优点:
1.提高代码的复用性 2.方便二次维护和二次开发
-
实现
// 二次封装ECharts import * as echarts from 'echarts'; // ECharts图表 import { useEffect } from 'react' // 处理用户传递的数据 const getEcharM = (prop: any) => { if (prop.datas) { if (prop.datas) { //用户给这个 echarts 传递 数据 return { xdata: [], ydata: [], dom: 'mains', symbol: "none", //标记的图形 lineStyleColor: '', areaStyleColor: '#007acc', opacity: 0.5, ...prop.datas, ...prop } } } } // 处理数据结构 const getSlist = (list: any, props: any) => { // 判断是否为多个系列 if (list) { // 表示多个系列 let arr: any = [] props.ydata.forEach((item: any, index: number) => { arr.push({ data: item.y, type: 'line', symbol: props.symbol, lineStyle: { color: props.lineStyleColor, width: 2 }, areaStyle: { color: props.areaStyleColor, opacity: props.opacity } }) }) return arr } else {// 表示单个系列 return { data: props.ydata, type: 'line', symbol: props.symbol, lineStyle: { color: props.lineStyleColor, width: 2, }, areaStyle: { color: props.areaStyleColor, opacity: props.opacity, } } } } // ECharts组件 function LineCom(prop: any) { let props: any = getEcharM(prop) // 用户传递的参数:返回一个对象 console.log('用户传递', prop); console.log('用户传递处理', props); // 监听 useEffect(() => { getEcharts() }, [prop]) // 生成可视化图表 const getEcharts = () => { // 获取元素,用来放置图表 let dom: any = document.getElementById(props.dom) // 创建echarts实例 var myChart = echarts.init(dom); // 配置项 => 绘制图表 myChart.setOption({ // 配置X轴 xAxis: { type: 'category', boundaryGap: false, data: props.xdata }, // 配置Y轴 yAxis: { type: 'value' }, series: getSlist(prop.seriesList, props) // 传入(用户给的用来判断是否为多个系列,处理过的用户传递的数据) }) } return ( <div className='lineCom'> <div id={props.dom} style={{ height: 350 + 'px', width: 100 + "%" }}></div> </div> ) } export default LineCom
// 使用二次封装的ECharts function Fenxi() { let list = ['黄金会员', '白金会员'] // 表示多个系列 let datasOne = { xdata: [1, 2, 3, 4, 5, 6, 7], ydata: [ { name: '黄金会员', y: [111, 222, 333, 444, 555, 666, 777] }, { name: '白金会员', y: [33, 22, 333, 444, 55, 66, 777] } ] } let datasTwo = { xdata: [1, 2, 3, 4, 5, 6, 7], ydata: [111, 432, 333, 234, 123, 452, 777] } return ( <div className='fenxiBox'> <div className='fenxiBoxItem'> <div>ECharts二次封装</div> <LineCom datas={datasOne} lineStyleColor="#dd4c35" seriesList={list} /> </div> <div className='fenxiBoxItem'> <div>ECharts二次封装</div> <LineCom datas={datasTwo} lineStyleColor="#1572b6" dom='ids' /> </div> </div> ) } export default Fenxi
监控页(二次封装表格组件)
-
思路
1.完成基本样式 2.确定动态属性
表格一行的数据:根据表头有多少个属性决定 表格有多少行数据:根据数据的长度决定 后端数据: 1、无需处理直接渲染; 2、处理后端数据: 在对应的表头项中添加render属性,属性值为一个方法,本质就是一个函数组件
-
实现
// 二次封装表格组件 import { Table } from 'antd'; import type { ColumnsType } from 'antd/es/table'; import ZeroPagination from "../ZeroPagination"; // 分页器 // 名称类型 interface DataType { key: string; // 唯一标识 MemberOrder: string; // 会员等级 MemberNumber: number; // 成交会员数 ratio: any; // 成交会员占比 payNum: any; // 支付订单数 tags: string[]; // 操作 } // 列 => 表头 const columns: ColumnsType<DataType> = [ { title: '会员等级', // 表头名称 dataIndex: 'MemberOrder', // 用来显示每一行的内容 key: 'MemberOrder', // 唯一标识 }, { title: '成交会员数', dataIndex: 'MemberNumber', key: 'MemberNumber', }, { title: '成交会员占比', dataIndex: 'ratio', key: 'ratio', }, { title: '支付订单数', key: 'payNum', dataIndex: 'payNum', }, { title: 'Action', key: 'action', // 该表头下的每一项可以自定义内容 render: () => ( <a>Delete</a> ), }, ]; // 表格组件 function ZeroTable(props: any) { // 触发父组件方法 const ongetData = (val: any) => { // console.log('子组件(表格)', val); props?.ongetData(val) } return ( <div className='table'> <h2>二次封装表格组件</h2> {/* 表格 */} <Table columns={columns} pagination={{ position: [] }} dataSource={props.list}></Table> {/* 分页器 */} <ZeroPagination total={props.list.length} ongetData={ongetData} /> </div> ) } export default ZeroTable;
// 分页器组件 import { Pagination } from 'antd'; // 引入分页器 function ZeroPagination(props: any) { // 页码发生改变,通过父组件让爷组件获取数据 const onChange = (e: any) => { // console.log('改变页码(孙组件分页器)', e); props?.ongetData(e) } return ( <Pagination defaultCurrent={1} total={props.total} defaultPageSize={3} onChange={(e) => onChange(e)} showSizeChanger /> ) } export default ZeroPagination
// 使用二次封装的表格组件 import './jiankong.scss' // 样式 import ZeroTable from "../../components/ZeroTable";// 引入二次封装的Table import { getTableData } from "../../https/api/jiangkong"; import { useState, useEffect } from "react"; // 监控页组件 function Jiankong() { let [tableList, setTableList] = useState([]) // 表格数据 // 发起请求获取数据 const getTable = (page = 1) => { getTableData({ page }).then((res: any) => { // console.log('表格数据', res.data); setTableList(res.data) }) } // 加载后生命周期 useEffect(() => { // 发起请求,获取表格数据 getTable() }, []) // 通过子组件,将方法传给孙组件(分页器) const ongetData = (val: any) => { // console.log('爷组件', val); getTable(val) // 获取数据 } return ( <div className="jiankong"> <ZeroTable list={tableList} ongetData={ongetData} /> </div> ) } export default Jiankong
权限设计页(按钮级权限)
-
思路
1.在项目中进行权限切换 本质:在登录页输入不同的用户得到不同的权限,将这些数据存放到本地或全局 2.上面的`权限设计`都是页面级权限 3.按钮级权限:不同用户可以看到同一个页面,但是页面里的内容不同 本质:条件判断 在src下新建一个文件,在文件中配置好当前项目中的所有按钮级的权限
-
扩展
1.项目中有多少权限: 后台管理系统:主管,人事,员工,行政,财务,经理... 2.项目的怎么实现权限: 2-1.页面级权限:根据不同的用户信息后去到不同的侧边栏导航 2-2.按钮级权限:不同用户可以看到同一个页面,但是页面里的内容不同 3.权限的特点: 3-1.动态生成路由 => 根据不同的用户信息动态生成路由信息 3-2.动态生成侧边栏导航 3-3.按钮级权限,实现一一对应
-
仓库
// 创建权限设计的reducer行为 let datas: any = { user: {}, // 用户信息 navList: [] // 侧边栏信息 } function limitData(state: any = datas, actions: any) { switch (actions.type) { case 'limitchange': let user = actions.user let navList = actions.navlist return { ...state, user, navList } default: return state } } export default limitData
-
utils
export function limitFind(val: any) { // 权限表 let promise = ['哆啦A梦', 'admin'] let find = promise.findIndex((item) => { return item == val }) if (find > -1) { return true } else { return false } }
-
权限页面
// 权限设置页 import "./limit.scss"; // 样式 import { Select } from 'antd'; // 选择器 import { useState } from "react"; import { changeLimit } from "../../https/api/limit"; // 引入请求 import { useDispatch, useSelector } from "react-redux"; import { limitFind } from "../../utils/antLimit"; function Limit() { let dispatch = useDispatch() // dispatch // 按钮级权限 let [show, setShow] = useState(false) // 获取本地存储的当前用户 let obj: any = sessionStorage.getItem('user') let user = JSON.parse(obj) // 获取store中的用户信息 let storeDataUser = useSelector((state: any) => state.LimitData.user) // value值发送变化时调用 const handleChange = (value: any) => { console.log('切换选项', value); // 发起请求获取数据 changeLimit({ name: value }).then((res: any) => { // console.log('权限接口数据', res.data); // 更该本地存储数据 sessionStorage.setItem('user', JSON.stringify(res.data.user)) sessionStorage.setItem('navList', JSON.stringify(res.data.navlist)) // 触发reducer行为 dispatch({ type: 'limitchange', user: res.data.user, navList: res.data.navlist }) // 按钮级权限 let anniu = limitFind(value) // 设置按钮级权限的显隐 setShow(anniu) }) } return ( <div className="limit"> <div className="nametext">当前用户: {storeDataUser.name || user.name}</div> <Select style={{ width: 120 }} defaultValue={storeDataUser.name || user.name} //默认选中的条目 onChange={handleChange} // value值发送变化时调用 options={[ // 选项数据 { value: '小张', label: '小张', }, { value: '哆啦A梦', label: '哆啦A梦', } ]} /> {/* 按钮级权限 */} {show ? <div>{storeDataUser.name || user.name}</div> : ''} </div> ) } export default Limit
- 处理头部的用户名称为响应式(store)
性能优化
-
react
模块处理将react内容进行模块化拆分,一个模块一个组件 因为react更新机制是:组件重新创建
-
避免使用内联函数
在编译模块的时候,react会将模块装成vnode(虚拟dom),里面所有的处理方法会合成集中处理 如果第一层触发,返回的是`useState`修改数据方法,这个方法是一个极其复杂的方法 我们应该使用嵌套函数返回一个自己写的方法,这样可以降低项目的性能损耗
-
Fragments
空的占位符<></> 这是一个空的双标签,是一个占位符 在项目中需要父子结构但是不确定使用什么类型的标签时使用
-
路由懒加载
作用:在使用了路由懒加载后,项目首次加载时,用户没有点过的懒加载页面,这个页面就不会加载,提高项目的加载速度 路由懒加载中包含了异步组件 语法:React.lazy() 配合 Suspense(首页白屏) 一起使用
-
组件的缓存
React.memo() 缓存组件,只有当这个缓存组件中的数据发生改变了才会重新创建 工作中一般不用useMemo,useCallback来做react的性能优化 何时用: 当组件存在子传父的时候就需要使用
-
动态生成路由、动态生成侧边栏导航
比如:项目有10个权限,对应的侧边栏导航是不同的,需要通过动态生成来减少项目的代码
-
将项目中的一些大的本地图片,上传到到云端(阿里云、七牛云),从而减少项目的体积
-
减少项目中的错误代码
-
列表渲染需要添加
key
,因为diff
算法