IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> JavaScript知识库 -> 聊聊React中的权限组件设计 -> 正文阅读

[JavaScript知识库]聊聊React中的权限组件设计

所谓的权限控制是什么?

一般后台管理系统的权限涉及到两种:

  • 资源权限
  • 数据权限

资源权限一般指菜单、页面、按钮等的可见权限。

数据权限一般指对于不同用户,同一页面上看到的数据不同。

本文主要是来探讨一下资源权限,也就是前端权限控制。这又分为了两部分:

  • 侧边栏菜单
  • 路由权限

在很多人的理解中,前端权限控制就是左侧菜单的可见与否,其实这是不对的。举一个例子,假设用户guest没有路由/setting的访问权限,但是他知道/setting的完整路径,直接通过输入路径的方式访问,此时仍然是可以访问的。这显然是不合理的。这部分其实就属于路由层面的权限控制。

实现思路

关于前端权限控制一般有两种方案:

  • 前端固定路由表和权限配置,由后端提供用户权限标识
  • 后端提供权限和路由信息结构接口,动态生成权限和菜单

我们这里采用的是第一种方案,服务只下发当前用户拥有的角色就可以了,路由表和权限的处理统一在前端处理。

整体实现思路也比较简单:现有权限(currentAuthority)和准入权限(authority)做比较,如果匹配则渲染和准入权限匹配的组件,否则渲染无权限组件(403 页面)

路由权限

既然是路由相关的权限控制,我们免不了先看一下当前的路由表:

{"name": "活动列表","path": "/activity-mgmt/list","key": "/activity-mgmt/list","exact": true,"authority": ["admin"],"component": ? LoadableComponent(props),"inherited": false,"hideInBreadcrumb": false
},
{"name": "优惠券管理","path": "/coupon-mgmt/coupon-rule-bplist","key": "/coupon-mgmt/coupon-rule-bplist","exact": true,"authority": ["admin","coupon"],"component": ? LoadableComponent(props),"inherited": true,"hideInBreadcrumb": false
},
{"name": "营销录入系统","path": "/marketRule-manage","key": "/marketRule-manage","exact": true,"component": ? LoadableComponent(props),"inherited": true,"hideInBreadcrumb": false
} 

这份路由表其实是我从控制台 copy 过来的,内部做了很多的转换处理,但最终生成的就是上面这个对象。

这里每一级菜单都加了一个authority字段来标识允许访问的角色。component代表路由对应的组件:

import React, { createElement } from "react"
import Loadable from "react-loadable"

"/activity-mgmt/list": {component: dynamicWrapper(app, ["activityMgmt"], () => import("../routes/activity-mgmt/list"))
},
// 动态引用组件并注册model
const dynamicWrapper = (app, models, component) => {// register modelsmodels.forEach(model => {if (modelNotExisted(app, model)) {// eslint-disable-next-lineapp.model(require(`../models/${model}`).default)}})// () => require('module')// transformed by babel-plugin-dynamic-import-node-sync// 需要将routerData塞到props中if (component.toString().indexOf(".then(") < 0) {return props => {return createElement(component().default, {...props,routerData: getRouterDataCache(app)})}}// () => import('module')return Loadable({loader: () => {return component().then(raw => {const Component = raw.default || rawreturn props =>createElement(Component, {...props,routerData: getRouterDataCache(app)})})},// 全局loadingloading: () => {return (<divstyle={{display: "flex",justifyContent: "center",alignItems: "center"}}><Spin size="large" className="global-spin" /></div>)}})
} 

有了路由表这份基础数据,下面就让我们来看下如何通过一步步的改造给原有系统注入权限。

先从src/router.js这个入口开始着手:

// 原src/router.js
import dynamic from "dva/dynamic"
import { Redirect, Route, routerRedux, Switch } from "dva/router"
import PropTypes from "prop-types"
import React from "react"
import NoMatch from "./components/no-match"
import App from "./routes/app"

const { ConnectedRouter } = routerRedux

const RouterConfig = ({ history, app }) => {const routes = [{path: "activity-management",models: () => [import("@/models/activityManagement")],component: () => import("./routes/activity-mgmt")},{path: "coupon-management",models: () => [import("@/models/couponManagement")],component: () => import("./routes/coupon-mgmt")},{path: "order-management",models: () => [import("@/models/orderManagement")],component: () => import("./routes/order-maint")},{path: "merchant-management",models: () => [import("@/models/merchantManagement")],component: () => import("./routes/merchant-mgmt")}// ...]return (<ConnectedRouter history={history}><App><Switch>{routes.map(({ path, ...dynamics }, key) => (<Routekey={key}path={`/${path}`}component={dynamic({app,...dynamics})}/>))}<Route component={NoMatch} /></Switch></App></ConnectedRouter>)
}

RouterConfig.propTypes = {history: PropTypes.object,app: PropTypes.object
}

export default RouterConfig 

这是一个非常常规的路由配置,既然要加入权限,比较合适的方式就是包一个高阶组件AuthorizedRoute。然后router.js就可以更替为:

function RouterConfig({ history, app }) {const routerData = getRouterData(app)const BasicLayout = routerData["/"].componentreturn (<ConnectedRouter history={history}><Switch><AuthorizedRoute path="/" render={props => <BasicLayout {...props} />} /></Switch></ConnectedRouter>)
} 

来看下AuthorizedRoute的大致实现:

const AuthorizedRoute = ({component: Component,authority,redirectPath,{...rest}
}) => {if (authority === currentAuthority) {return (<Route{...rest}render={props => <Component {...props} />} />)} else {return (<Route {...rest} render={() =><Redirect to={redirectPath} />} />)}
} 

我们看一下这个组件有什么问题:页面可能允许多个角色访问,用户拥有的角色也可能是多个(可能是字符串,也可呢是数组)。

直接在组件中判断显然不太合适,我们把这部分逻辑抽离出来:

/**
 * 通用权限检查方法
 * Common check permissions method
 * @param { 菜单访问需要的权限 } authority
 * @param { 当前角色拥有的权限 } currentAuthority
 * @param { 通过的组件 Passing components } target
 * @param { 未通过的组件 no pass components } Exception
 */
const checkPermissions = (authority, currentAuthority, target, Exception) => {console.log("checkPermissions -----> authority", authority)console.log("currentAuthority", currentAuthority)console.log("target", target)console.log("Exception", Exception)// 没有判定权限.默认查看所有// Retirement authority, return target;if (!authority) {return target}// 数组处理if (Array.isArray(authority)) {// 该菜单可由多个角色访问if (authority.indexOf(currentAuthority) >= 0) {return target}// 当前用户同时拥有多个角色if (Array.isArray(currentAuthority)) {for (let i = 0; i < currentAuthority.length; i += 1) {const element = currentAuthority[i]// 菜单访问需要的角色权限 < ------ > 当前用户拥有的角色if (authority.indexOf(element) >= 0) {return target}}}return Exception}// string 处理if (typeof authority === "string") {if (authority === currentAuthority) {return target}if (Array.isArray(currentAuthority)) {for (let i = 0; i < currentAuthority.length; i += 1) {const element = currentAuthority[i]if (authority.indexOf(element) >= 0) {return target}}}return Exception}throw new Error("unsupported parameters")
}

const check = (authority, target, Exception) => {return checkPermissions(authority, CURRENT, target, Exception)
} 

首先如果路由表中没有authority字段默认都可以访问。

接着分别对authority为字符串和数组的情况做了处理,其实就是简单的查找匹配,匹配到了就可以访问,匹配不到就返回Exception,也就是我们自定义的异常页面。

有一个点一直没有提:用户当前角色权限 currentAuthority 如何获取?这个是在页面初始化时从接口读取,然后存到 store

有了这块逻辑,我们对刚刚的AuthorizedRoute做一下改造。首先抽象一个Authorized组件,对权限校验逻辑做一下封装:

import React from "react"
import CheckPermissions from "./CheckPermissions"

class Authorized extends React.Component {render() {const { children, authority, noMatch = null } = this.propsconst childrenRender = typeof children === "undefined" ? null : childrenreturn CheckPermissions(authority, childrenRender, noMatch)}
}

export default Authorized 

接着AuthorizedRoute可直接使用Authorized组件:

import React from "react"
import { Redirect, Route } from "react-router-dom"
import Authorized from "./Authorized"

class AuthorizedRoute extends React.Component {render() {const { component: Component, render, authority, redirectPath, ...rest } = this.propsreturn (<Authorizedauthority={authority}noMatch={<Route {...rest} render={() => <Redirect to={{ pathname: redirectPath }} />} />}><Route {...rest} render={props => (Component ? <Component {...props} /> : render(props))} /></Authorized>)}
}

export default AuthorizedRoute 

这里采用了render props的方式:如果提供了component props就用component渲染,否则使用render渲染。

菜单权限

菜单权限的处理相对就简单很多了,统一集成到SiderMenu组件处理:

export default class SiderMenu extends PureComponent {constructor(props) {super(props)}/** * get SubMenu or Item */getSubMenuOrItem = item => {if (item.children && item.children.some(child => child.name)) {const childrenItems = this.getNavMenuItems(item.children)// 当无子菜单时就不展示菜单if (childrenItems && childrenItems.length > 0) {return (<SubMenutitle={item.icon ? (<span>{getIcon(item.icon)}<span>{item.name}</span></span>) : (item.name)}key={item.path}>{childrenItems}</SubMenu>)}return null}return <Menu.Item key={item.path}>{this.getMenuItemPath(item)}</Menu.Item>}/** * 获得菜单子节点 * @memberof SiderMenu */getNavMenuItems = menusData => {if (!menusData) {return []}return menusData.filter(item => item.name && !item.hideInMenu).map(item => {// make domconst ItemDom = this.getSubMenuOrItem(item)return this.checkPermissionItem(item.authority, ItemDom)}).filter(item => item)}/** * * @description 菜单权限过滤 * @param {*} authority * @param {*} ItemDom * @memberof SiderMenu */checkPermissionItem = (authority, ItemDom) => {const { Authorized } = this.propsif (Authorized && Authorized.check) {const { check } = Authorizedreturn check(authority, ItemDom)}return ItemDom}render() {// ...return<Sidertrigger={null}collapsiblecollapsed={collapsed}breakpoint="lg"onCollapse={onCollapse}className={siderClass}><div className="logo"><Link to="/home" className="logo-link">{!collapsed && <h1>冯言冯语</h1>}</Link></div><Menukey="Menu"theme={theme}mode={mode}{...menuProps}onOpenChange={this.handleOpenChange}selectedKeys={selectedKeys}>{this.getNavMenuItems(menuData)}</Menu></Sider>}
} 

这里我只贴了一些核心代码,其中的checkPermissionItem就是实现菜单权限的关键。他同样用到了上文中的check方法来对当前菜单进行权限比对,如果没有权限就直接不展示当前菜单。

最后

最近找到一个VUE的文档,它将VUE的各个知识点进行了总结,整理成了《Vue 开发必须知道的36个技巧》。内容比较详实,对各个知识点的讲解也十分到位。



有需要的小伙伴,可以点击下方卡片领取,无偿分享

  JavaScript知识库 最新文章
ES6的相关知识点
react 函数式组件 & react其他一些总结
Vue基础超详细
前端JS也可以连点成线(Vue中运用 AntVG6)
Vue事件处理的基本使用
Vue后台项目的记录 (一)
前后端分离vue跨域,devServer配置proxy代理
TypeScript
初识vuex
vue项目安装包指令收集
上一篇文章      下一篇文章      查看所有文章
加:2022-10-17 12:24:08  更:2022-10-17 12:24:14 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/17 16:45:03-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码