共计 24304 个字符,预计需要花费 61 分钟才能阅读完成。
导航
[[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 中实现权限管制
-
我的项目代码已上传到 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').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…