导航
[React 从零实践01-后台] 代码分割
[React 从零实践02-后台] 权限控制
[React 从零实践03-后台] 自定义hooks
[React 从零实践04-后台] docker-compose 部署react+egg+nginx+mysql
[React 从零实践05-后台] Gitlab-CI使用Docker自动化部署
[源码-webpack01-前置知识] AST抽象语法树
[源码-webpack02-前置知识] Tapable
[源码-webpack03] 手写webpack - compiler简单编译流程
[源码] Redux React-Redux01
[源码] axios
[源码] vuex
[源码-vue01] data响应式 和 初始化渲染
[源码-vue02] computed 响应式 - 初始化,访问,更新过程
[源码-vue03] watch 侦听属性 - 初始化和更新
[源码-vue04] Vue.set 和 vm.$set
[源码-vue05] Vue.extend
[源码-vue06] Vue.nextTick 和 vm.$nextTick
[部署01] Nginx
[部署02] Docker 部署vue项目
[部署03] gitlab-CI
[深入01] 执行上下文
[深入02] 原型链
[深入03] 继承
[深入04] 事件循环
[深入05] 柯里化 偏函数 函数记忆
[深入06] 隐式转换 和 运算符
[深入07] 浏览器缓存机制(http缓存机制)
[深入08] 前端安全
[深入09] 深浅拷贝
[深入10] Debounce Throttle
[深入11] 前端路由
[深入12] 前端模块化
[深入13] 观察者模式 发布订阅模式 双向数据绑定
[深入14] canvas
[深入15] webSocket
[深入16] webpack
[深入17] http 和 https
[深入18] CSS-interview
[深入19] 手写Promise
[深入20] 手写函数
[深入21] 算法 - 查找和排序
前置知识
(1) 一些单词
graph:图,图表
intelligence:智能的
contrast:对比
persistence:持久化
( data persistence:数据持久化 )
(2) 权限控制的类型
- 登陆权限控制
- 是否登陆
登陆才能访问的页面/路由
不登陆就可以访问的页面/路由,比如 login 页面
- 是否登陆
- 页面权限控制
- 菜单
菜单中的页面/路由是否显示
如果只是控制菜单,还不够,因为如果注册了所有路由,即使菜单隐藏,还是可以通过地址栏访问到
- 页面
页面的路由是否注册
退一步,如果不根据权限就行路由注册,即使注册了所有路由,没权限就从定向到404,这样虽然不是最好,但也能用
- 按钮
页面中的按钮(增、删、改、查)是否显示
- 菜单
- 接口权限控制
- 兜底
路由可能配置失误,按钮可能忘了加权限,这种时候请求控制可以用来兜底,越权请求将在前端被拦截
通过axios请求响应拦截来实现
- 兜底
(3) react-router-dom 中的 Redirect 组件
Redirect => to => state
当to属性是一个对象时 state 属性可以传递一个对象,在to页面中可以通过 this.props.state 获取,应用场景:比如重定向到login页面,登陆成功后要返回之前所在的页面,就可以把当前的location信息通过state带入到login页面
(4) react-router-dom 实现在 Form 未保存时跳转别的路由提示
- (
Prompt
) 组件 和 (router.getUserConfirmation
) 配合 -
Prompt
-
message 属性:
字符串
或者函数
- 函数
- 返回true,允许跳转
- 返回false,不允许跳转,没有任何提示
- 返回字符串,会弹出是否可以跳转的弹窗,提示就是字符串内的内容,确定和取消
- 字符串
- 将上面的返回字符串
- 函数
-
when:boolean
- true:弹窗
- false:顺利跳转
-
message 属性:
-
router.getUserConfirmation(message, callback)
- 问题:为什么需要getUserConfirmation?
- 因为:Prompt默认使用window.confirm,丑,可以通过getUserConfirmation自定义样式DOM,阻止默认弹窗
- 参数:
- messag:就是Prompt的message指定的字符串
- callback:true允许跳转,false不允许跳转
在表单组件中使用 Prompt
<Prompt message={() => isSave ? true : '表单还未保存,真的需要跳转吗?'} ></Prompt>
ReactDOM.render(
<Provider store={store}>
<Router getUserConfirmation={getUserConfirmation}> // ----------- getUserConfirmation
<App />
</Router>
</Provider>,
document.getElementById('root')
);
function getUserConfirmation(message: string, callback: any) {
Modal.confirm({ // ----------------------------------------------- antd Modal
content: message, // ------------------------------------------- message就是Pormpt组件的message返回的字符串
cancelText: '取消',
okText: '确定',
onCancel: () => {
callback(false) // ------------------------------------------- callback(false) 不跳转
},
onOk: () => {
callback(true) // -------------------------------------------- callback(true) 跳转
}
})
}
(5) react-router-config 源码分析
为啥要分析 react-router-config
因为做路由权限时,需要向route配置对象中添加一些权限相关的自定义属性,但我们又想用集中式路由来管理
react-router-config => renderRoutes 源码分析
renderRoutes 一个最重要的api
----
import React from "react";
import { Switch, Route } from "react-router";
function renderRoutes(routes, extraProps = {}, switchProps = {}) {
return routes ? (
<Switch {...switchProps}>
{routes.map((route, i) => (
<Route
key={route.key || i}
path={route.path}
exact={route.exact}
strict={route.strict}
render={props =>
route.render ? (
route.render({ ...props, ...extraProps, route: route })
) : (
<route.component {...props} {...extraProps} route={route} />
)
}
/>
))}
</Switch>
) : null;
}
export default renderRoutes;
解析:
1. renderRoutes()只遍历一层routes,不管你嵌套多少层routes数组,你都需要在对应的组件中再次调用renderRoutes()传入该层该routes
2. 所以:在每层的render和componet两个属性中,都需要传入该层的route配置对象,在组件中通过props.route.routes获取该层的routes (重要)
3. exact和strict都是boolean类型的数据,所以当配置对象中不存在这两个属性时,boolen相当于传入false即不生效
4. render属性是一个函数,(routesProps) => {...} ,routeProps包含 match, location and history
(6) antd4版本以上 自定义图标组件
import { createFromIconfontCN } from '@ant-design/icons';
const MyIcon = createFromIconfontCN({
scriptUrl: '//at.alicdn.com/t/font_8d5l8fzk5b87iudi.js', // 在 iconfont.cn 上生成 => Symbol方式!!!!!
});
ReactDOM.render(<MyIcon type="icon-example" />, mountedNode);
(7) 添加别名 @
映射 src
在TS的项目中
- create-react-app构建的项目,eject后,找到 config/webpack.config.js => resolve.alias
- tsconfig.json 中删除
baseUrl
和paths
,添加"extends": "./paths.json"
- tsconfig.json 中删除
- 在根目录新建
paths.json
文件,写入baseUrl
和paths
配置
- 在根目录新建
- 教程地址
1. webpack.config.js => resolve => alias
module.export = {
resolve: {
alias: {
"@": path.resolve(__dirname, '../src')
}
}
}
2. 根目录新建 paths.json 写入以下配置
{
"compilerOptions": {
"baseUrl": "src",
"paths": {
"@/*": ["*"]
}
}
}
3. 在 tsconfig.json 中做如下修改,添加( extends ), 删除( baseUrl,paths )
{
// "baseUrl": "src",
// "paths": {
// "@/*": ["src/*"]
// },
"extends": "./paths.json"
}
(8) create-react-app 配置全局的 scss ,而不需要每次 @import
- 安装
sass-resources-loader
- 修改 config/webpack.config.js 如下
注意:很多教程修改use:getStyleLoaders().concat()这样修改不行
const getStyleLoaders = (cssOptions, preProcessor) => {
const loaders = [......].filter(Boolean);
if (preProcessor) {
loaders.push(......);
}
if (preProcessor === 'sass-loader') { // ------------ 如果第二个参数是 sass-loader,就 push sass-resources-loader
loaders.push({
loader: 'sass-resources-loader',
options: {
resources: [
// 这里按照你的文件路径填写../../../ 定位到根目录下, 可以引入多个文件
path.resolve(__dirname, '../src/style/index.scss'),
]
}
})
}
return loaders;
};
(9) eslint 检查 react-hooks 语法
- eslint-plugin-react-hooks
比如:可以检查 hooks 不能在循环,条件等地方使用,不能在回调中使用等等
- 安装:yarn add eslint-plugin-react-hooks --dev
- 使用:在
.eslintrc.js
中 添加plugin
和rules
配置
/* eslint-disable */
module.exports = {
"env": {
"es6": true, // 在开发环境,启用es6语法,包括全局变量
"node": true,
"browser": true
},
"parser": "babel-eslint", // 解析器
"parserOptions": { // 解析器选项
"ecmaVersion": 6, // 启用es6语法,不包括全局变量
"sourceType": "module",
"ecmaFeatures": { //额外的语言特性
"jsx": true // 启用jsx语法
}
},
"plugins": [
// ...
"react-hooks"
],
rules: {
'no-console': 'off', // 可以console
'no-debugger': 'off',
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn"
},
}
/* eslint-disable */
(一) react中实现权限控制
(1) ( 嵌套路由注册 ) 和 ( menu ) 和 ( breadcrumb面包屑 ) 共用同一份 ( routes )
-
好处
- 路由注册的path和menu的path共用一个,而不用分开维护
- 集中式路由,统一维护,虽然router4是分布式路由思想
-
注意点
- ( menu根据不同权限显示隐藏 ) 和 ( 路由根据权限注册和不注册 ) 是两个概念,如果只是控制menu的显示隐藏,而所有的路由都注册的话,即使页面上没有出现别的权限的菜单,但是通过地址栏输入地址等方式还是可以导航到路由注册的页面,这就需要不在权限的路由不注册或者跳转到404页面或者做提示没权限等处理
- ( 子菜单 ) 用 ( subs ) 数组属性表示,( 嵌套路由 ) 用 ( routes ) 数组属性表示
- menu是树形菜单,所以注册路由时要递归遍历注册每一层,menu中有子菜单我们用
subs
表示 - 如果menu的item存在subs,则该item层级不应该有
path
和component
属性 - ( 即只有menu.item有上面这两个属性,submenu没有,因为不需要显示和跳转 )
- 全局下renderRoutes遍历一次routes,即只注册第一层的routes,嵌套路由存在routes属性,在相应的路由页面中再次调用renderRoutes注册路由,但是再递归遍历所有的menu相关的subs进行路由注册
- 代码
- routes
routes是这样一个数组
----
const totalRoutes: IRouteModule[] = [
{
path: '/login',
component: Login,
},
{
path: '/404',
component: NotFound,
},
{
path: '/',
component: Layout,
routes: [
// routes:用于嵌套路由,注意不是嵌套菜单
// subs:主要还遍历注册menu树形菜单,和渲染menu树形菜单,在不同系统的路由中定义了subs
// ----------------------------------------------------------- 嵌套路由通过 renderRoutes 做处理
...adminRoutes, // ------------------------------------ ( 后台系统路由 ),单独维护,同时用于menu
...bigScreenRoutes, // -------------------------------- ( 大屏系统路由 ),单独维护,同时用于menu
]
}
]
---- 分割线 ----
const adminRoutes: IRouteModule[] = [{
// ---------------- adminRoutes 用于menu的树形菜单的 ( 渲染 )和 ( 路由注册,注册可以在同一层级,因为mune视口一样 )
title: '首页',
icon: 'anticon-home--line',
key: '/admin-home',
path: '/admin-home',
component: AdminHome,
}, {
title: 'UI',
icon: 'anticon-uikit',
key: '/admin-ui',
subs: [{
// -------------------------------------------------------- subs用于注册路由,并且用于menu树形菜单的渲染
// -------------------------------------------------------- ( 路由注册:其实就是在不同的地方渲染 <Route /> 组件 )
// -------------------------------------------------------- ( 菜单渲染:其实就是menu菜单在页面上显示 )
title: 'Antd',
icon: 'anticon-ant-design',
key: '/admin-ui/antd',
subs: [{
title: '首页',
icon: 'anticon-codev1',
key: '/admin-ui/antd/index',
path: '/admin-ui/antd/index',
component: UiAntd,
}]
}, {
title: 'Vant',
icon: 'anticon-relevant-outlined',
key: '/admin-ui/vant',
path: '/admin-ui/vant',
component: UiAntd,
}]
}]
- renderRoutes - 重点
import React from 'react'
import { IRouteModule } from '../../global/interface'
import { Switch, Route } from 'react-router-dom'
/**
* @function normolize
* @description 递归的对route.subs做normalize,即把所有嵌套展平到一层,主要对menu树就行路由注册
* @description 因为menu树都在同一个路由视口,所以可以在同一层级就行路由注册
* @description 注意:path 和 component 在存在subs的那层menu-route对象中同时存在和同时不存在
*/
function normolize(routes?: IRouteModule[]) {
let result: IRouteModule[] = []
routes?.forEach(route => {
!route.subs
? result.push(route)
: result = result.concat(normolize(route.subs)) // ---------------- 拼接
})
return result
}
/**
* @function renderRoutes
* @description 注册所有路由,并向嵌套子路由组件传递 route 对象属性,子组件就可以获取嵌套路由属性 routes
*/
const renderRoutes = (routes?: IRouteModule[], extraProps = {}, switchProps = {}) => {
return routes
? <Switch {...switchProps}>
{normolize(routes).map((route, index) => { // --------------------- 先对subs做处理,再map
return route.path && route.component &&
// path 并且 component 同时存在才进行路由注册
// path 和 componet 总是同时存在,同时不存在
<Route
key={route.key || `${index + +new Date()}`}
path={route.path}
exact={route.exact}
strict={route.strict}
render={props => {
return route.render
? route.render({ ...props, ...extraProps, route: route })
: <route.component {...props} {...extraProps} route={route} />
// 向嵌套组件中传递 route属性,通过route.routes在嵌套路由组件中可以再注册嵌套路由
}} />
})}
</Switch>
: null
}
export {
renderRoutes
}
- menu
/**
* @function renderMenu
* @description 递归渲染菜单
*/
const renderMenu = (adminRoutes: IRouteModule[]) => {
return adminRoutes.map(({ subs, key, title, icon }) => {
return subs
?
<SubMenu key={key} title={title} icon={<IconFont type={icon || 'anticon-shouye'} />}>
{renderMenu(subs)}
</SubMenu>
:
<Menu.Item key={key} icon={<IconFont type={icon || 'anticon-shouye'} />} >{title}</Menu.Item>
})
}
- 嵌套路由
<Layout className={styles.layoutAdmin}>
// -------------------------------------------------------------------------------- Layout 是 '/' 路由对应的组件
// ---------------- {renderRoutes(props.route.routes)} 就是在 '/' 路由中渲染的 <Route path="" compoent="" />组件
<Sider>
<Menu
mode="inline"
theme="dark"
onClick={goPage}
>
{renderMenu(adminRoutes)}
</Menu>
</Sider>
<Layout>
<Header className={styles.header}>
<ul className={styles.topMenu}>
<li>退出</li>
</ul>
</Header>
<Content className={styles.content}>
{renderRoutes(props.route.routes)} // --------------- 再次执行,注册嵌套的路由,成为父组件的子组件
</Content>
</Layout>
</Layout>
(2) 在(1)的基础上加入权限 ( 登陆,页面,菜单 )
-
要达到的效果 ( 菜单和路由两个方面考虑 )
- menu根据权限显示和隐藏
注意menu中由于存在树形,为了控制粒度更细,在 submenu 和 menu.item 上都加入权限的判断比较好
- router根据权限注册和不注册
- menu根据权限显示和隐藏
-
需要添加的字段
-
needLoginAuth:boolen
- 表示路由/菜单是否需要登陆权限
- ( 只要登陆,后端就会返回角色,不同角色的权限可以用rolesAuth数组表示,如果返回的角色在rolesAuth数组中,就注册路由 或 显示菜单)
如果 needLoginAuth是false,则就不需要有 rolesAuth 字段了,即任何角色都会有的路由或菜单
-
rolesAuth:array
- 该路由注册/菜单显示 需要的角色数组
-
meta: object
- 可以把
needLoginAuth
和rolesAuth
放入meta
对象中,便于管理
- 可以把
-
visiable
- visiable主要用于 list 和 detail 这两种类型的页面,详情页在menu中是不展示的,但是需要注册Route,需要用字段来判断隐藏掉详情页
-
needLoginAuth:boolen
-
模拟需求
- 角色有两种:user 和 admin
- 菜单权限
- 首页:登陆后,两种角色都可以访问
- UI:
- ui 这个菜单两种角色都显示
- ui/antd 这个菜单只有 admin 可以访问和显示
- ui/vant 这个菜单两种角色都可以显示
- JS:
- 只有admin可以显示
- 代码
- 改造后的routes
const totalRoutes: IRouteModule[] = [
{
path: '/login',
component: Login,
meta: {
needLoginAuth: false
}
},
{
path: '/404',
component: NotFound,
meta: {
needLoginAuth: false
}
},
{
path: '/',
component: Layout,
meta: {
needLoginAuth: true,
rolesAuth: ['user', 'admin']
},
routes: [
// routes:用于嵌套路由,注意不是嵌套菜单
// subs:主要还遍历注册menu树形菜单,和渲染menu树形菜单,在不同系统的路由中定义了subs
// 嵌套路由通过 renderRoutes函数 做处理
...adminRoutes, // --------------------------- 后台系统路由表
...bigScreenRoutes, // ----------------------- 大屏系统路由表
]
}
]
---- 分割线 ----
const adminRoutes: IRouteModule[] = [{
title: '首页',
icon: 'anticon-home--line',
key: '/admin-home',
path: '/admin-home',
component: AdminHome,
meta: {
needLoginAuth: true,
rolesAuth: ['user', 'admin']
},
}, {
title: 'UI',
icon: 'anticon-uikit',
key: '/admin-ui',
meta: {
needLoginAuth: true,
rolesAuth: ['user', 'admin']
},
subs: [{ // subs用于注册路由,并且用于menu树形菜单
title: 'Antd',
icon: 'anticon-ant-design',
key: '/admin-ui/antd',
meta: {
needLoginAuth: true,
rolesAuth: ['user','admin']
},
subs: [{
title: '首页',
icon: 'anticon-codev1',
key: '/admin-ui/antd/index',
path: '/admin-ui/antd/index',
component: UiAntd,
meta: {
needLoginAuth: true,
rolesAuth: ['user', 'admin']
},
}, {
title: 'Form表单',
icon: 'anticon-yewubiaodan',
key: '/admin-ui/antd/form',
path: '/admin-ui/antd/form',
component: UiAntdForm,
meta: {
needLoginAuth: true,
rolesAuth: ['admin']
},
}]
}, {
title: 'Vant',
icon: 'anticon-relevant-outlined',
key: '/admin-ui/vant',
path: '/admin-ui/vant',
component: UiVant,
meta: {
needLoginAuth: true,
rolesAuth: ['user', 'admin']
},
}]
}, {
title: 'JS',
icon: 'anticon-js',
key: '/admin-js',
meta: {
needLoginAuth: true,
rolesAuth: ['user', 'admin']
},
subs: [{
title: 'ES6',
icon: 'anticon-6',
key: '/admin-js/es6',
path: '/admin-js/es6',
component: JsEs6,
meta: {
needLoginAuth: true,
rolesAuth: ['user', 'admin']
},
}, {
title: 'ES5',
icon: 'anticon-js',
key: '/admin-js/es5',
path: '/admin-js/es5',
component: UiAntd,
meta: {
needLoginAuth: true,
rolesAuth: ['user', 'admin']
},
}]
}]
- 对routes和menu过滤的函数
/**
* @function routesFilter routes的权限过滤
*/
export function routesFilter(routes: IRouteModule[], roles: string) {
return routes.filter(({ meta: { needLoginAuth, rolesAuth }, routes: nestRoutes, subs }) => {
if (nestRoutes) { // 存在routes,对routes数组过滤,并重新赋值过滤后的routes
nestRoutes = routesFilter(nestRoutes, roles) // 递归
}
if (subs) { // 存在subs,对subs数组过滤,并重新赋值过滤后的subs
subs = routesFilter(subs, roles) // 递归
}
return !needLoginAuth
? true
: rolesAuth?.includes(roles)
? true
: false
})
}
- renderRoutes 登陆权限的验证,路由注册过滤即路由注册权限,menu的过滤显示隐藏不在这里进行
/**
* @function renderRoutes
* @description 注册所有路由,并向嵌套子路由组件传递 route 对象属性,子组件就可以获取嵌套路由属性 routes
*/
const renderRoutes = (routes: IRouteModule[], extraProps = {}, switchProps = {}) => {
const history = useHistory()
const token = useSelector((state: {app: {loginMessage: {token: string}}}) => state.app.loginMessage.token)
const roles = useSelector((state: {app: {loginMessage: {roles: string}}}) => state.app.loginMessage.roles)
if (!token) {
history.push('/login') // token未登录去登陆页面,即登陆权限的验证!!!!!!!!!!!!!!!!!!!!
}
routes = routesFilter(routes, roles) // 权限过滤,这里只用于路由注册,menu过滤还需在menu页面调用routesFilter
routes = normalize(routes) // 展平 subs
return routes
? <Switch {...switchProps}>
{
routes.map((route, index) => { // 先对subs做处理
return route.path && route.component &&
// path 并且 component 同时存在才进行路由注册
// path 和 componet 总是同时存在,同时不存在
<Route
key={route.key || `${index + +new Date()}`}
path={route.path}
exact={route.exact}
strict={route.strict}
render={props => {
return route.render
? route.render({ ...props, ...extraProps, route: route })
: <route.component {...props} {...extraProps} route={route} />
// 向嵌套组件中传递 route属性,通过route.routes在嵌套路由组件中可以再注册嵌套路由
}} />
})}
</Switch>
: null
}
- menu的过滤
/**
* @function renderMenu
* @description 递归渲染菜单
*/
const renderMenu = (adminRoutes: IRouteModule[]) => {
const roles =
useSelector((state: { app: { loginMessage: { roles: string } } }) => state.app.loginMessage.roles) ||
getLocalStorage('loginMessage').roles;
// 这里用 eslint-plugin-react-hooks 会报错,因为 hooks 必须放在最顶层
// useSelector
adminRoutes = routesFilter(adminRoutes, roles) // adminRoutes权限过滤!!!!!!!!!!!!!!!!!!!!!
return adminRoutes.map(({ subs, key, title, icon }) => {
return subs
?
<SubMenu key={key} title={title} icon={<IconFont type={icon || 'anticon-shouye'} />}>
{renderMenu(subs)}
</SubMenu>
:
<Menu.Item key={key} icon={<IconFont type={icon || 'anticon-shouye'} />} >{title}</Menu.Item>
})
}
(3) breadcrumb 面包屑
-
面包屑要解决的基本问题
- 对于导航到详情页的动态路由,要显示到面包屑
- 对于有menu.item即routes中有component的route对象,要能够点击并导航
- 对于submenu的item不能点击,并置灰
- 如何判断是否可以点击? 如果routes具有subs数组,就不可以点击;只有menu.item的route可以点击
- 因为面包屑是根据当前的url的pathname来进行判断的,所以无需做持久化,只要刷新地址栏不变就不会变
但是有点需要注意:就是退出登陆时,应该清除掉 localStorage 中的用于缓存menu等所有数据,而刷新时候不需要,如果退出时不清除localStorage,登陆重定向到首页,就会加载首页的面包屑和缓存的menu,造成不匹配
import { Breadcrumb } from 'antd'
import React from 'react'
import { useHistory, useLocation } from 'react-router-dom'
import styles from './breadcrumb.module.scss'
import { routesFilter } from '@/utils/render-routes/index'
import adminRoutes from '@/router/admin-routes'
import { useSelector } from 'react-redux'
import { IRouteModule } from '@/global/interface'
import { getLocalStorage } from '@/utils'
import _ from 'lodash'
const CustomBreadcrumb = () => {
const roles = useSelector((state: any) => state.app.loginMessage.roles) || getLocalStorage('loginMessage').roles
const pathname = useLocation().pathname // 获取url的path
const history = useHistory()
// routeParams => 获取useParams的params对象,对象中包含动态路由的id属性
const routeParams = getLocalStorage('routeParams')
// 深拷贝 权限过滤后的adminRoutes
const routesAmin = _.cloneDeep([...routesFilter(adminRoutes, roles)]) // 权限过滤,为了和menu同步
// generateRouteMap => 生成面包屑的 path,title映射
const generateRouteMap = (routesAmin: IRouteModule[]) => {
const routeMap = {}
function step(routesAmin: IRouteModule[]) {
routesAmin.forEach((item, index) => {
if (item.path.includes(Object.keys(routeParams)[0])) { // 动态路由存在:符号,缓存该 route,用于替换面包屑的最后一级名字
item.path = item.path.replace(`:${Object.keys(routeParams)[0]}`, routeParams[Object.keys(routeParams)[0]])
// 把动态路由参数(:id) 替换成真实的(params)
}
routeMap[item.path] = item.title
item.subs && step(item.subs)
})
}
step(routesAmin) // 用于递归
return routeMap
}
const routeMap = generateRouteMap(routesAmin)
// generateBreadcrumbData => 生成面包屑的data
const generateBreadcrumbData = (pathname: string) => {
const arr = pathname.split('/')
return arr.map((item, index) => {
return arr.slice(0, index + 1).join('/')
}).filter(v => !!v)
}
const data = generateBreadcrumbData(pathname)
// pathFilter
// 面包屑是否可以点击导航
// 同时用来做可点击,不可点击的 UI
const pathFilter = (path: string) => {
// normalizeFilterdAdminRoutes => 展平所有subs
function normalizeFilterdAdminRoutes(routesAmin: IRouteModule[]) {
let normalizeArr: IRouteModule[] = []
routesAmin.forEach((item, index: number) => {
item.subs
?
normalizeArr = normalizeArr.concat(normalizeFilterdAdminRoutes(item.subs))
:
normalizeArr.push(item)
})
return normalizeArr
}
const routes = normalizeFilterdAdminRoutes(_.cloneDeep(routesAmin))
// LinkToWhere => 是否可以点击面包屑
function LinkToWhere(routes: IRouteModule[]) {
let isCanGo = false
routes.forEach(item => {
if (item.path === path && item.component) {
isCanGo = true
}
})
return isCanGo
}
return LinkToWhere(routes)
}
// 点击时的导航操作
const goPage = (item: string) => {
pathFilter(item) && history.push(item)
// 函数组合,可以点击就就跳转
}
// 渲染 breadcrumb
const renderData = (item: string, index: number) => {
return (
<Breadcrumb.Item key={index} onClick={() => goPage(item)}>
<span
style={{
cursor: pathFilter(item) ? 'pointer' : 'not-allowed',
color: pathFilter(item) ? '#4DB2FF' : 'silver'
}}
>
{routeMap[item]}
</span>
</Breadcrumb.Item>
)
}
return (
<Breadcrumb className={styles.breadcrumb} separator="/">
{data.map(renderData)}
</Breadcrumb>
)
}
export default CustomBreadcrumb
-
上面的面包屑存在的问题:
需求:面包屑在点击到详情时,更新全局面包屑
不足:使用localstore,在子组件set,在父组件get,但是父组件先执行,子组件后执行,并且localstore不会更新组件,所以导致面包屑不更新
代替:在子组件 es6detail 中 dispatch 了一个action,但不是在onClick的事件中,触发了警告
// 需求:面包屑在点击到详情时,更新全局面包屑
// 不足:使用localstore,在子组件set,在父组件get,但是父组件先执行,子组件后执行,并且localstore不会更新组件,所以导致面包屑不更新
// 代替:在子组件 es6detail 中 dispatch 了一个action,但不是在onClick的事件中,触发了警告
// 之所以还这样做,是要在子组件es6detail更新后,b更新CustomBreadcrumb
// 因为子组件es6detail更新了store,而父组件 CustomBreadcrumb 有引用store中的state,所以会更新
// 不足:触发了警告
const CustomBreadcrumb = () => {
const roles = useSelector((state: any) => state.app.loginMessage.roles) || getLocalStorage('loginMessage').roles
const pathname = useLocation().pathname // 获取url的path
const history = useHistory()
// routeParams => 获取useParams的params对象,对象中包含动态路由的id属性
const routeParams = getLocalStorage('routeParams')
// debugger
// 深拷贝 权限过滤后的adminRoutes
const routesAmin = _.cloneDeep([...routesFilter(adminRoutes, roles)]) // 权限过滤,为了和menu同步
// generateRouteMap => 生成面包屑的 path,title映射
const generateRouteMap = (routesAmin: IRouteModule[]) => {
const routeMap = {}
function step(routesAmin: IRouteModule[]) {
routesAmin.forEach((item, index) => {
if (item.path.includes(routeParams && Object.keys(routeParams)[0])) { // 动态路由存在:符号,缓存该 route,用于替换面包屑的最后一级名字
item.path = item.path.replace(`:${Object.keys(routeParams)[0]}`, routeParams[Object.keys(routeParams)[0]])
// 把动态路由参数(:id) 替换成真实的(params)
}
routeMap[item.path] = item.title
item.subs && step(item.subs)
})
}
step(routesAmin) // 用于递归
return routeMap
}
const routeMap = generateRouteMap(routesAmin)
// generateBreadcrumbData => 生成面包屑的data
const generateBreadcrumbData = (pathname: string) => {
const arr = pathname.split('/')
return arr.map((item, index) => {
return arr.slice(0, index + 1).join('/')
}).filter(v => !!v)
}
const data = generateBreadcrumbData(pathname)
// pathFilter
// 面包屑是否可以点击导航
// 同时用来做可点击,不可点击的 UI
const pathFilter = (path: string) => {
// normalizeFilterdAdminRoutes => 展平所有subs
function normalizeFilterdAdminRoutes(routesAmin: IRouteModule[]) {
let normalizeArr: IRouteModule[] = []
routesAmin.forEach((item, index: number) => {
item.subs
?
normalizeArr = normalizeArr.concat(normalizeFilterdAdminRoutes(item.subs))
:
normalizeArr.push(item)
})
return normalizeArr
}
const routes = normalizeFilterdAdminRoutes(_.cloneDeep(routesAmin))
// LinkToWhere => 是否可以点击面包屑
function LinkToWhere(routes: IRouteModule[]) {
let isCanGo = false
routes.forEach(item => {
if (item.path === path && item.component) {
isCanGo = true
}
})
return isCanGo
}
return LinkToWhere(routes)
}
// 点击时的导航操作
const goPage = (item: string) => {
pathFilter(item) && history.push(item)
// 函数组合,可以点击就就跳转
}
// 渲染 breadcrumb
const renderData = (item: string, index: number) => {
return (
<Breadcrumb.Item key={index} onClick={() => goPage(item)}>
<span
style={{
cursor: pathFilter(item) ? 'pointer' : 'not-allowed',
color: pathFilter(item) ? '#4DB2FF' : 'silver'
}}
>
{routeMap[item]}
</span>
</Breadcrumb.Item>
)
}
return (
<Breadcrumb className={styles.breadcrumb} separator="/">
{data.map(renderData)}
</Breadcrumb>
)
}
export default CustomBreadcrumb
(4) menu数据持久化
- 相关属性
openKeys
onOpenChange()
selectedKeys
onClick()
- 存入localStorage,在effect中初始化
import React, { useEffect, useState } from 'react'
import { renderRoutes, routesFilter } from '@/utils/render-routes/index'
import styles from './index.module.scss'
import { Button, Layout, Menu } from 'antd';
import adminRoutes from '@/router/admin-routes'
import { IRouteModule } from '@/global/interface'
import IconFont from '@/components/Icon-font'
import { useHistory } from 'react-router-dom';
import { useSelector } from 'react-redux';
import { getLocalStorage, setLocalStorage } from '@/utils';
import CustomBreadcrumb from '@/components/custorm-breadcrumb';
import { MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons';
const { SubMenu } = Menu;
const { Header, Sider, Content } = Layout;
const Admin = (props: any) => {
const [collapsed, setcollapsed] = useState(false)
const [selectedKeys, setSelectedKeys] = useState(['/admin-home'])
const [openKeys, setOpenKeys]: any = useState(['/admin-home'])
const history = useHistory()
useEffect(() => {
// 初始化,加载持久化的 selectedKeys 和 openKeys
const selectedKeys = getLocalStorage('selectedKeys')
const openKeys = getLocalStorage('openKeys')
setSelectedKeys(v => v = selectedKeys)
setOpenKeys((v: any) => v = openKeys)
}, [])
/**
* @function renderMenu
* @description 递归渲染菜单
*/
const renderMenu = (adminRoutes: IRouteModule[]) => {
const roles =
useSelector((state: { app: { loginMessage: { roles: string } } }) => state.app.loginMessage.roles) ||
getLocalStorage('loginMessage').roles;
const adminRoutesDeepClone = routesFilter([...adminRoutes], roles) // adminRoutes权限过滤
return adminRoutesDeepClone.map(({ subs, key, title, icon, path }) => {
return subs
?
<SubMenu key={key} title={title} icon={<IconFont type={icon || 'anticon-shouye'} />}>
{renderMenu(subs)}
</SubMenu>
:
!path.includes(':') && <Menu.Item key={key} icon={<IconFont type={icon || 'anticon-shouye'} />} >{title}</Menu.Item>
// 动态路由不进行显示,因为一般动态路由是详情页
// 虽然不显示,但是需要注册路由,只是menu不显示
})
}
// 点击 menuItem 触发的事件
const goPage = ({ keyPath, key }: { keyPath: any[], key: any }) => {
history.push(keyPath[0])
setSelectedKeys(v => v = [key])
setLocalStorage('selectedKeys', [key]) // 记住当前点击的item,刷新持久化
}
// 展开/关闭的回调
const onOpenChange = (openKeys: any) => {
setOpenKeys((v: any) => v = openKeys)
setLocalStorage('openKeys', openKeys) // 记住展开关闭的组,刷新持久化
}
const toggleCollapsed = () => {
setcollapsed(v => v = !v)
};
return (
<Layout className={styles.layoutAdmin}>
<Sider collapsed={collapsed}>
<Menu
mode="inline"
theme="dark"
onClick={goPage}
// inlineCollapsed={} 在有 Sider 包裹的情况下,需要在Sider中设置展开隐藏
inlineIndent={24}
selectedKeys={selectedKeys}
openKeys={openKeys}
onOpenChange={onOpenChange}
>
{renderMenu([...adminRoutes])}
</Menu>
</Sider>
<Layout>
<Header className={styles.header}>
<aside>
<span onClick={toggleCollapsed}>
{collapsed
? <MenuUnfoldOutlined className={styles.toggleCollapsedIcon} />
: <MenuFoldOutlined className={styles.toggleCollapsedIcon} />
}
</span>
</aside>
<ul className={styles.topMenu}>
<li onClick={() => history.push('/login')}>退出</li>
</ul>
</Header>
<Content className={styles.content}>
<CustomBreadcrumb />
{renderRoutes(props.route.routes)}
{/* renderRoutes(props.route.routes) 再次执行,注册嵌套的路由,成为父组件的子组件 */}
</Content>
</Layout>
</Layout>
)
}
export default Admin
项目源码
资料
react路由鉴权(完善) https://juejin.im/post/6844903924441284615
快速打造react管理系统(项目) https://juejin.im/post/6844903981945208839
权限控制的类型 https://juejin.im/post/6844903882338861063
React-Router实现前端路由鉴权:https://juejin.im/post/6857055615739985933
react-router-config路由鉴权:https://github.com/leishihong/react-router-config