导航
[[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 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 官网
- 为啥要分析 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中实现权限控制
(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').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/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...
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。