关于javascript:React-从零实践02后台-权限控制

47次阅读

共计 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 的我的项目中

    1. create-react-app 构建的我的项目,eject 后,找到 config/webpack.config.js => resolve.alias
    1. tsconfig.json 中删除 baseUrlpaths,增加 "extends": "./paths.json"
    1. 在根目录新建 paths.json 文件,写入 baseUrlpaths 配置
  • 教程地址

  • 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 中 增加 pluginrules 配置

    /* 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 层级不应该有 pathcomponent 属性
    • (即只有 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 依据权限注册和不注册
  • 须要增加的字段

    • needLoginAuth:boolen

      • 示意路由 / 菜单是否须要登陆权限
      • (只有登陆,后端就会返回角色,不同角色的权限能够用 rolesAuth 数组示意,如果返回的角色在 rolesAuth 数组中,就注册路由 或 显示菜单)
      • 如果 needLoginAuth 是 false,则就不须要有 rolesAuth 字段了,即任何角色都会有的路由或菜单
    • rolesAuth:array

      • 该路由注册 / 菜单显示 须要的角色数组
    • meta: object

      • 能够把 needLoginAuthrolesAuth 放入 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…

正文完
 0