导航

[[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...


woow_wu7
10 声望2 粉丝