导航
[[react] Hooks](https://juejin.im/post/684490...)
[[React 从零实际01-后盾] 代码宰割](https://juejin.im/post/687902...)
[[React 从零实际02-后盾] 权限管制](https://juejin.im/post/688148...)
[[React 从零实际03-后盾] 自定义hooks](https://juejin.im/post/688713...)
[[React 从零实际04-后盾] docker-compose 部署react+egg+nginx+mysql](https://juejin.im/post/689239...)
[[React 从零实际05-后盾] Gitlab-CI应用Docker自动化部署](https://juejin.cn/post/689788...)
[[源码-webpack01-前置常识] AST形象语法树](https://juejin.im/post/684490...)
[[源码-webpack02-前置常识] Tapable](https://juejin.im/post/684490...)
[[源码-webpack03] 手写webpack - compiler简略编译流程](https://juejin.im/post/684490...)
[[源码] Redux React-Redux01](https://juejin.im/post/684490...)
[[源码] axios ](https://juejin.im/post/684490...)
[[源码] vuex ](https://juejin.im/post/684490...)
[[源码-vue01] data响应式 和 初始化渲染 ](https://juejin.im/post/684490...)
[[源码-vue02] computed 响应式 - 初始化,拜访,更新过程 ](https://juejin.im/post/684490...)
[[源码-vue03] watch 侦听属性 - 初始化和更新 ](https://juejin.im/post/684490...)
[[源码-vue04] Vue.set 和 vm.$set](https://juejin.im/post/684490...)
[[源码-vue05] Vue.extend](https://juejin.im/post/684490...)
[[源码-vue06] Vue.nextTick 和 vm.$nextTick](https://juejin.im/post/684790...)
[[部署01] Nginx](https://juejin.im/post/684490...)
[[部署02] Docker 部署vue我的项目](https://juejin.im/post/684490...)
[[部署03] gitlab-CI](https://juejin.im/post/684490...)
[[深刻01] 执行上下文](https://juejin.im/post/684490...)
[[深刻02] 原型链](https://juejin.im/post/684490...)
[[深刻03] 继承](https://juejin.im/post/684490...)
[[深刻04] 事件循环](https://juejin.im/post/684490...)
[[深刻05] 柯里化 偏函数 函数记忆](https://juejin.im/post/684490...)
[[深刻06] 隐式转换 和 运算符](https://juejin.im/post/684490...)
[[深刻07] 浏览器缓存机制(http缓存机制)](https://juejin.im/post/684490...)
[[深刻08] 前端平安](https://juejin.im/post/684490...)
[[深刻09] 深浅拷贝](https://juejin.im/post/684490...)
[[深刻10] Debounce Throttle](https://juejin.im/post/684490...)
[[深刻11] 前端路由](https://juejin.im/post/684490...)
[[深刻12] 前端模块化](https://juejin.im/post/684490...)
[[深刻13] 观察者模式 公布订阅模式 双向数据绑定](https://juejin.im/post/684490...)
[[深刻14] canvas](https://juejin.im/post/684490...)
[[深刻15] webSocket](https://juejin.im/post/684490...)
[[深刻16] webpack](https://juejin.im/post/684490...)
[[深刻17] http 和 https](https://juejin.im/post/684490...)
[[深刻18] CSS-interview](https://juejin.im/post/684490...)
[[深刻19] 手写Promise](https://juejin.im/post/684490...)
[[深刻20] 手写函数](https://juejin.im/post/684490...)
[[深刻21] 算法 - 查找和排序](https://juejin.cn/post/690714...)
前置常识
(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:顺利跳转
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 Modalcontent: message, // ------------------------------------------- message就是Pormpt组件的message返回的字符串cancelText: '勾销',okText: '确定',onCancel: () => {callback(false) // ------------------------------------------- callback(false) 不跳转},onOk: () => {callback(true) // -------------------------------------------- callback(true) 跳转}})}
(5) react-router-config 源码剖析
- 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;
- renderRoutes()只遍历一层routes,不论你嵌套多少层routes数组,你都须要在对应的组件中再次调用renderRoutes()传入该层该routes
- 所以:在每层的render和componet两个属性中,都须要传入该层的route配置对象,在组件中通过props.route.routes获取该层的routes (重要)
- exact和strict都是boolean类型的数据,所以当配置对象中不存在这两个属性时,boolen相当于传入false即不失效
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
配置
- 在根目录新建
教程地址
webpack.config.js => resolve => alias
module.export = {
resolve: {
alias: {"@": path.resolve(__dirname, '../src')
}
}
}根目录新建 paths.json 写入以下配置
{
"compilerOptions": {
"baseUrl": "src",
"paths": {"@/*": ["*"]
}
}
}在 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中实现权限管制
我的项目代码已上传到github,具体代码请自行钻研
(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,须要用字段来判断暗藏掉详情页
模仿需要
- 角色有两种: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').rolesconst pathname = useLocation().pathname // 获取url的pathconst history = useHistory()// routeParams => 获取useParams的params对象,对象中蕴含动静路由的id属性const routeParams = getLocalStorage('routeParams')// 深拷贝 权限过滤后的adminRoutesconst 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 => 生成面包屑的dataconst 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 // 面包屑是否能够点击导航 // 同时用来做可点击,不可点击的 UIconst 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) // 函数组合,能够点击就就跳转}// 渲染 breadcrumbconst 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').rolesconst pathname = useLocation().pathname // 获取url的pathconst history = useHistory()// routeParams => 获取useParams的params对象,对象中蕴含动静路由的id属性const routeParams = getLocalStorage('routeParams')// debugger// 深拷贝 权限过滤后的adminRoutesconst 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 => 生成面包屑的dataconst 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 // 面包屑是否能够点击导航// 同时用来做可点击,不可点击的 UIconst pathFilter = (path: string) => {// normalizeFilterdAdminRoutes => 展平所有subsfunction 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)// 函数组合,能够点击就就跳转}// 渲染 breadcrumbconst 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/684490...
疾速打造react管理系统(我的项目) https://juejin.im/post/684490...
权限管制的类型 https://juejin.im/post/684490...
React-Router实现前端路由鉴权:https://juejin.im/post/685705...
react-router-config路由鉴权:https://github.com/leishihong...