导航

[[react] Hooks](https://juejin.im/post/684490...)

[[React 从零实际01-后盾] 代码宰割](https://juejin.im/post/687902...)
[[React 从零实际02-后盾] 权限管制](https://juejin.im/post/688148...)
[[React 从零实际03-后盾] 自定义hooks](https://juejin.im/post/688713...)
[[React 从零实际04-后盾] docker-compose 部署react+egg+nginx+mysql](https://juejin.im/post/689239...)
[[React 从零实际05-后盾] Gitlab-CI应用Docker自动化部署](https://juejin.cn/post/689788...)

[[源码-webpack01-前置常识] AST形象语法树](https://juejin.im/post/684490...)
[[源码-webpack02-前置常识] Tapable](https://juejin.im/post/684490...)
[[源码-webpack03] 手写webpack - compiler简略编译流程](https://juejin.im/post/684490...)
[[源码] Redux React-Redux01](https://juejin.im/post/684490...)
[[源码] axios ](https://juejin.im/post/684490...)
[[源码] vuex ](https://juejin.im/post/684490...)
[[源码-vue01] data响应式 和 初始化渲染 ](https://juejin.im/post/684490...)
[[源码-vue02] computed 响应式 - 初始化,拜访,更新过程 ](https://juejin.im/post/684490...)
[[源码-vue03] watch 侦听属性 - 初始化和更新 ](https://juejin.im/post/684490...)
[[源码-vue04] Vue.set 和 vm.$set](https://juejin.im/post/684490...)
[[源码-vue05] Vue.extend](https://juejin.im/post/684490...)

[[源码-vue06] Vue.nextTick 和 vm.$nextTick](https://juejin.im/post/684790...)

[[部署01] Nginx](https://juejin.im/post/684490...)
[[部署02] Docker 部署vue我的项目](https://juejin.im/post/684490...)
[[部署03] gitlab-CI](https://juejin.im/post/684490...)

[[深刻01] 执行上下文](https://juejin.im/post/684490...)
[[深刻02] 原型链](https://juejin.im/post/684490...)
[[深刻03] 继承](https://juejin.im/post/684490...)
[[深刻04] 事件循环](https://juejin.im/post/684490...)
[[深刻05] 柯里化 偏函数 函数记忆](https://juejin.im/post/684490...)
[[深刻06] 隐式转换 和 运算符](https://juejin.im/post/684490...)
[[深刻07] 浏览器缓存机制(http缓存机制)](https://juejin.im/post/684490...)
[[深刻08] 前端平安](https://juejin.im/post/684490...)
[[深刻09] 深浅拷贝](https://juejin.im/post/684490...)
[[深刻10] Debounce Throttle](https://juejin.im/post/684490...)
[[深刻11] 前端路由](https://juejin.im/post/684490...)
[[深刻12] 前端模块化](https://juejin.im/post/684490...)
[[深刻13] 观察者模式 公布订阅模式 双向数据绑定](https://juejin.im/post/684490...)
[[深刻14] canvas](https://juejin.im/post/684490...)
[[深刻15] webSocket](https://juejin.im/post/684490...)
[[深刻16] webpack](https://juejin.im/post/684490...)
[[深刻17] http 和 https](https://juejin.im/post/684490...)
[[深刻18] CSS-interview](https://juejin.im/post/684490...)
[[深刻19] 手写Promise](https://juejin.im/post/684490...)
[[深刻20] 手写函数](https://juejin.im/post/684490...)
[[深刻21] 算法 - 查找和排序](https://juejin.cn/post/690714...)

前置常识

(1) 一些单词

graph:图,图表intelligence:智能的contrast:比照persistence:长久化( data persistence:数据长久化 )

(2) 权限管制的类型

  • 登陆权限管制

    • 是否登陆

      • 登陆能力拜访的页面/路由
      • 不登陆就能够拜访的页面/路由,比方 login 页面
  • 页面权限管制

    • 菜单

      • 菜单中的页面/路由是否显示
      • 如果只是管制菜单,还不够,因为如果注册了所有路由,即便菜单暗藏,还是能够通过地址栏拜访到
    • 页面

      • 页面的路由是否注册
      • 退一步,如果不依据权限就行路由注册,即便注册了所有路由,没权限就从定向到404,这样尽管不是最好,但也能用
    • 按钮

      • 页面中的按钮(增、删、改、查)是否显示
  • 接口权限管制

    • 兜底

      • 路由可能配置失误,按钮可能忘了加权限,这种时候申请管制能够用来兜底,越权申请将在前端被拦挡
      • 通过axios申请响应拦挡来实现

(3) react-router-dom 中的 Redirect 组件

  • Redirect => to => state
  • 当to属性是一个对象时 state 属性能够传递一个对象,在to页面中能够通过 this.props.state 获取,利用场景:比方重定向到login页面,登陆胜利后要返回之前所在的页面,就能够把以后的location信息通过state带入到login页面

(4) react-router-dom 实现在 Form 未保留时跳转别的路由提醒

  • ( Prompt ) 组件 和 ( router.getUserConfirmation ) 配合
  • Prompt

    • message 属性:字符串 或者 函数

      • 函数

        • 返回true,容许跳转
        • 返回false,不容许跳转,没有任何提醒
        • 返回字符串,会弹出是否能够跳转的弹窗,提醒就是字符串内的内容,确定和勾销
      • 字符串

        • 将下面的返回字符串
    • when:boolean

      • true:弹窗
      • false:顺利跳转
  • router.getUserConfirmation(message, callback)

    • 问题:为什么须要getUserConfirmation?
    • 因为:Prompt默认应用window.confirm,丑,能够通过getUserConfirmation自定义款式DOM,阻止默认弹窗
    • 参数:

      • messag:就是Prompt的message指定的字符串
      • callback:true容许跳转,false不容许跳转

        在表单组件中应用 Prompt<Prompt message={() => isSave ? true : '表单还未保留,真的须要跳转吗?'} ></Prompt>ReactDOM.render(<Provider store={store}><Router getUserConfirmation={getUserConfirmation}> // ----------- getUserConfirmation<App /></Router></Provider>,document.getElementById('root'));function getUserConfirmation(message: string, callback: any) {Modal.confirm({ // ----------------------------------------------- antd Modalcontent: message, // ------------------------------------------- message就是Pormpt组件的message返回的字符串cancelText: '勾销',okText: '确定',onCancel: () => {callback(false) // ------------------------------------------- callback(false) 不跳转},onOk: () => {callback(true) // -------------------------------------------- callback(true) 跳转}})}

(5) react-router-config 源码剖析

  • react-router-config 官网
  • 为啥要剖析 react-router-config
  • 因为做路由权限时,须要向route配置对象中增加一些权限相干的自定义属性,但咱们又想用集中式路由来治理
  • react-router-config => renderRoutes 源码剖析

    renderRoutes 一个最重要的api----import React from "react";import { Switch, Route } from "react-router";function renderRoutes(routes, extraProps = {}, switchProps = {}) {return routes ? (  <Switch {...switchProps}>    {routes.map((route, i) => (      <Route        key={route.key || i}        path={route.path}        exact={route.exact}        strict={route.strict}        render={props =>          route.render ? (            route.render({ ...props, ...extraProps, route: route })          ) : (            <route.component {...props} {...extraProps} route={route} />          )        }      />    ))}  </Switch>) : null;} export default renderRoutes;
  • renderRoutes()只遍历一层routes,不论你嵌套多少层routes数组,你都须要在对应的组件中再次调用renderRoutes()传入该层该routes
  • 所以:在每层的render和componet两个属性中,都须要传入该层的route配置对象,在组件中通过props.route.routes获取该层的routes (重要)
  • exact和strict都是boolean类型的数据,所以当配置对象中不存在这两个属性时,boolen相当于传入false即不失效
  • render属性是一个函数,(routesProps) => {...} ,routeProps蕴含 match, location and history

(6) antd4版本以上 自定义图标组件

import { createFromIconfontCN } from '@ant-design/icons';const MyIcon = createFromIconfontCN({  scriptUrl: '//at.alicdn.com/t/font_8d5l8fzk5b87iudi.js', // 在 iconfont.cn 上生成 => Symbol形式!!!!!});ReactDOM.render(<MyIcon type="icon-example" />, mountedNode);

(7) 增加别名 @ 映射 src 在TS的我的项目中

    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').rolesconst pathname = useLocation().pathname // 获取url的pathconst history = useHistory()// routeParams => 获取useParams的params对象,对象中蕴含动静路由的id属性const routeParams = getLocalStorage('routeParams')// 深拷贝 权限过滤后的adminRoutesconst routesAmin = _.cloneDeep([...routesFilter(adminRoutes, roles)]) // 权限过滤,为了和menu同步// generateRouteMap => 生成面包屑的 path,title映射const generateRouteMap = (routesAmin: IRouteModule[]) => {  const routeMap = {}  function step(routesAmin: IRouteModule[]) {    routesAmin.forEach((item, index) => {      if (item.path.includes(Object.keys(routeParams)[0])) { // 动静路由存在:符号,缓存该 route,用于替换面包屑的最初一级名字        item.path = item.path.replace(`:${Object.keys(routeParams)[0]}`, routeParams[Object.keys(routeParams)[0]])        // 把动静路由参数(:id) 替换成实在的(params)      }      routeMap[item.path] = item.title      item.subs && step(item.subs)    })  }  step(routesAmin) // 用于递归  return routeMap}const routeMap = generateRouteMap(routesAmin)// generateBreadcrumbData => 生成面包屑的dataconst generateBreadcrumbData = (pathname: string) => {  const arr = pathname.split('/')  return arr.map((item, index) => {    return arr.slice(0, index + 1).join('/')  }).filter(v => !!v)}const data = generateBreadcrumbData(pathname)// pathFilter   // 面包屑是否能够点击导航  // 同时用来做可点击,不可点击的 UIconst pathFilter = (path: string) => {  // normalizeFilterdAdminRoutes => 展平所有subs  function normalizeFilterdAdminRoutes(routesAmin: IRouteModule[]) {    let normalizeArr: IRouteModule[] = []    routesAmin.forEach((item, index: number) => {      item.subs        ?        normalizeArr = normalizeArr.concat(normalizeFilterdAdminRoutes(item.subs))        :        normalizeArr.push(item)    })    return normalizeArr  }  const routes = normalizeFilterdAdminRoutes(_.cloneDeep(routesAmin))  // LinkToWhere => 是否能够点击面包屑  function LinkToWhere(routes: IRouteModule[]) {    let isCanGo = false    routes.forEach(item => {      if (item.path === path && item.component) {        isCanGo = true      }    })    return isCanGo  }  return LinkToWhere(routes)}// 点击时的导航操作const goPage = (item: string) => {  pathFilter(item) && history.push(item)  // 函数组合,能够点击就就跳转}// 渲染 breadcrumbconst renderData = (item: string, index: number) => {  return (    <Breadcrumb.Item key={index} onClick={() => goPage(item)}>      <span        style={{          cursor: pathFilter(item) ? 'pointer' : 'not-allowed',          color: pathFilter(item) ? '#4DB2FF' : 'silver'        }}      >        {routeMap[item]}      </span>    </Breadcrumb.Item>  )}return (  <Breadcrumb className={styles.breadcrumb} separator="/">    {data.map(renderData)}  </Breadcrumb>)}export default CustomBreadcrumb
  • 下面的面包屑存在的问题

    • 需要:面包屑在点击到详情时,更新全局面包屑
    • 有余:应用localstore,在子组件set,在父组件get,然而父组件先执行,子组件后执行,并且localstore不会更新组件,所以导致面包屑不更新
    • 代替:在子组件 es6detail 中 dispatch 了一个action,但不是在onClick的事件中,触发了正告

      // 需要:面包屑在点击到详情时,更新全局面包屑// 有余:应用localstore,在子组件set,在父组件get,然而父组件先执行,子组件后执行,并且localstore不会更新组件,所以导致面包屑不更新// 代替:在子组件 es6detail 中 dispatch 了一个action,但不是在onClick的事件中,触发了正告// 之所以还这样做,是要在子组件es6detail更新后,b更新CustomBreadcrumb// 因为子组件es6detail更新了store,而父组件 CustomBreadcrumb 有援用store中的state,所以会更新// 有余:触发了正告const CustomBreadcrumb = () => {const roles = useSelector((state: any) => state.app.loginMessage.roles) || getLocalStorage('loginMessage').rolesconst pathname = useLocation().pathname // 获取url的pathconst history = useHistory()// routeParams => 获取useParams的params对象,对象中蕴含动静路由的id属性const routeParams = getLocalStorage('routeParams')// debugger// 深拷贝 权限过滤后的adminRoutesconst routesAmin = _.cloneDeep([...routesFilter(adminRoutes, roles)]) // 权限过滤,为了和menu同步// generateRouteMap => 生成面包屑的 path,title映射const generateRouteMap = (routesAmin: IRouteModule[]) => {const routeMap = {}function step(routesAmin: IRouteModule[]) {  routesAmin.forEach((item, index) => {    if (item.path.includes(routeParams && Object.keys(routeParams)[0])) { // 动静路由存在:符号,缓存该 route,用于替换面包屑的最初一级名字      item.path = item.path.replace(`:${Object.keys(routeParams)[0]}`, routeParams[Object.keys(routeParams)[0]])      // 把动静路由参数(:id) 替换成实在的(params)    }    routeMap[item.path] = item.title    item.subs && step(item.subs)  })}step(routesAmin) // 用于递归return routeMap}const routeMap = generateRouteMap(routesAmin)// generateBreadcrumbData => 生成面包屑的dataconst generateBreadcrumbData = (pathname: string) => {const arr = pathname.split('/')return arr.map((item, index) => {  return arr.slice(0, index + 1).join('/')}).filter(v => !!v)}const data = generateBreadcrumbData(pathname)// pathFilter // 面包屑是否能够点击导航// 同时用来做可点击,不可点击的 UIconst pathFilter = (path: string) => {// normalizeFilterdAdminRoutes => 展平所有subsfunction normalizeFilterdAdminRoutes(routesAmin: IRouteModule[]) {  let normalizeArr: IRouteModule[] = []  routesAmin.forEach((item, index: number) => {    item.subs      ?      normalizeArr = normalizeArr.concat(normalizeFilterdAdminRoutes(item.subs))      :      normalizeArr.push(item)  })  return normalizeArr}const routes = normalizeFilterdAdminRoutes(_.cloneDeep(routesAmin))// LinkToWhere => 是否能够点击面包屑function LinkToWhere(routes: IRouteModule[]) {  let isCanGo = false  routes.forEach(item => {    if (item.path === path && item.component) {      isCanGo = true    }  })  return isCanGo}return LinkToWhere(routes)}// 点击时的导航操作const goPage = (item: string) => {pathFilter(item) && history.push(item)// 函数组合,能够点击就就跳转}// 渲染 breadcrumbconst renderData = (item: string, index: number) => {return (  <Breadcrumb.Item key={index} onClick={() => goPage(item)}>    <span      style={{        cursor: pathFilter(item) ? 'pointer' : 'not-allowed',        color: pathFilter(item) ? '#4DB2FF' : 'silver'      }}    >      {routeMap[item]}    </span>  </Breadcrumb.Item>)}return (<Breadcrumb className={styles.breadcrumb} separator="/">  {data.map(renderData)}</Breadcrumb>)}export default CustomBreadcrumb

(4) menu数据长久化

  • 相干属性

    • openKeys
    • onOpenChange()
    • selectedKeys
    • onClick()
  • 存入localStorage,在effect中初始化

    import React, { useEffect, useState } from 'react'import { renderRoutes, routesFilter } from '@/utils/render-routes/index'import styles from './index.module.scss'import { Button, Layout, Menu } from 'antd';import adminRoutes from '@/router/admin-routes'import { IRouteModule } from '@/global/interface'import IconFont from '@/components/Icon-font'import { useHistory } from 'react-router-dom';import { useSelector } from 'react-redux';import { getLocalStorage, setLocalStorage } from '@/utils';import CustomBreadcrumb from '@/components/custorm-breadcrumb';import { MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons';const { SubMenu } = Menu;const { Header, Sider, Content } = Layout;const Admin = (props: any) => {  const [collapsed, setcollapsed] = useState(false)  const [selectedKeys, setSelectedKeys] = useState(['/admin-home'])  const [openKeys, setOpenKeys]: any = useState(['/admin-home'])  const history = useHistory()  useEffect(() => {      // 初始化,加载长久化的 selectedKeys 和 openKeys      const selectedKeys = getLocalStorage('selectedKeys')      const openKeys = getLocalStorage('openKeys')      setSelectedKeys(v => v = selectedKeys)      setOpenKeys((v: any) => v = openKeys)  }, [])  /**   * @function renderMenu   * @description 递归渲染菜单   */  const renderMenu = (adminRoutes: IRouteModule[]) => {      const roles =          useSelector((state: { app: { loginMessage: { roles: string } } }) => state.app.loginMessage.roles) ||          getLocalStorage('loginMessage').roles;      const adminRoutesDeepClone = routesFilter([...adminRoutes], roles) // adminRoutes权限过滤      return adminRoutesDeepClone.map(({ subs, key, title, icon, path }) => {          return subs              ?              <SubMenu key={key} title={title} icon={<IconFont type={icon || 'anticon-shouye'} />}>                  {renderMenu(subs)}              </SubMenu>              :              !path.includes(':') && <Menu.Item key={key} icon={<IconFont type={icon || 'anticon-shouye'} />} >{title}</Menu.Item>          // 动静路由不进行显示,因为个别动静路由是详情页          // 尽管不显示,然而须要注册路由,只是menu不显示      })  }  // 点击 menuItem 触发的事件  const goPage = ({ keyPath, key }: { keyPath: any[], key: any }) => {      history.push(keyPath[0])      setSelectedKeys(v => v = [key])      setLocalStorage('selectedKeys', [key]) // 记住以后点击的item,刷新长久化  }  // 开展/敞开的回调  const onOpenChange = (openKeys: any) => {      setOpenKeys((v: any) => v = openKeys)      setLocalStorage('openKeys', openKeys) // 记住开展敞开的组,刷新长久化  }  const toggleCollapsed = () => {      setcollapsed(v => v = !v)  };  return (      <Layout className={styles.layoutAdmin}>          <Sider collapsed={collapsed}>              <Menu                  mode="inline"                  theme="dark"                  onClick={goPage}                  // inlineCollapsed={} 在有 Sider 包裹的状况下,须要在Sider中设置开展暗藏                  inlineIndent={24}                  selectedKeys={selectedKeys}                  openKeys={openKeys}                  onOpenChange={onOpenChange}              >                  {renderMenu([...adminRoutes])}              </Menu>          </Sider>          <Layout>              <Header className={styles.header}>                  <aside>                      <span onClick={toggleCollapsed}>                          {collapsed                              ? <MenuUnfoldOutlined className={styles.toggleCollapsedIcon} />                              : <MenuFoldOutlined className={styles.toggleCollapsedIcon} />                          }                      </span>                  </aside>                  <ul className={styles.topMenu}>                      <li onClick={() => history.push('/login')}>退出</li>                  </ul>              </Header>              <Content className={styles.content}>                  <CustomBreadcrumb />                  {renderRoutes(props.route.routes)}                  {/* renderRoutes(props.route.routes) 再次执行,注册嵌套的路由,成为父组件的子组件 */}              </Content>          </Layout>      </Layout>  )}export default Admin

我的项目源码

  • 我的项目源码
  • 部署成果预览地址

材料

react路由鉴权(欠缺) https://juejin.im/post/684490...
疾速打造react管理系统(我的项目) https://juejin.im/post/684490...
权限管制的类型 https://juejin.im/post/684490...
React-Router实现前端路由鉴权:https://juejin.im/post/685705...
react-router-config路由鉴权:https://github.com/leishihong...