解决什么问题
本文主要是自己在实际业务开发中的一些总结, 写出来希望与大家一起探讨.
首先介绍一下业务背景:
我们开发的是一套 2B 的企业培训 SaaS 系统, 企业可以在平台上用直播的方式对自己的员工进行培训.
这套 SaaS 系统可以对接不同的平台, 如钉钉, 微信等等(不同平台会限制一些功能, 如钉钉不能显示员工手机号), 也可以进行内网部署(内网会关闭一些线上功能). 由于部署环境和对接系统的不同, 平台所能使用的功能受限, 对应的前端权限也不一样.
这里前端的开发主要涉及账户系统级的培训管理和单个房间内直播时的控制两个部分, 它们在一个 SPA 里面, 需要一套系统管理两个部分的权限.
培训管理会分为账户管理员, 子管理员(后续可能会增加系统管理角色), 直播控制人员分为讲师, 嘉宾, 助手等角色. 这里所有的人员都可能在一个控制页, 但是由于角色的不同, UI 也会不一样.
综上, 在不同部署平台下, 不同级别 (角色) 的人员在同一个房间里, 他们所看到的界面和能使用的功能是不一样的. 而且一个角色受限于部署平台和主管理员所购买的平台服务, 或者随着主管理员关闭 / 开放某些功能, 看到的界面也会不一样.
所以我们需要做一套权限管理系统来综合处理这些信息(平台, 账户, 角色), 保证各角色看到不同的界面. 可以看到我们这里说的权限已经不仅限于简单的角色权限, 还包括角色之上的平台和账户管理员的限制.
因为最后的权限取决于所登录的账户, 所以在开发中, 我们将权限和账户信息放到了一起, 统称为 metaConfig, 即账户元信息, 包含账户名字, 角色等基本信息, 所属主账号, 具体角色信息, 角色权限等, 它将决定最终的界面显示.
如何解决
我们使用 React 和 Redux 来开发, metaConfig 对象可以直接存在 Redux 中进行管理.
在视图组件中可以通过 connect 函数, 将 metaConfig 里配置的属性或权限数据映射成各组件所需的视图数据, 最终呈现出不同的界面.
在路由控制器内, 也可以从 Redux 中拿到 metaConfig 来决定路由权限.
一些请求方法的权限也可以根据对应的 metaConfig 属性来决定.
在我们的系统中, metaConfig 的处理是放在 reducer 中的, 会有一个默认的 defaultMetaConfig, 通过 reducer 生成最后的 metaConfig. 权限管理最关键的就是如何生成各个角色对应的 metaConfig, 总结起来就是:
metaConfig=f(defaultMetaConfig)
分层(管道处理)
把复杂问题拆分成简单问题是开发中的一个重要手段. 这里我们可以通过分层处理的方式, 将权限管理拆分成多个层级, 每层对应一个处理模块. 这样一个大的权限处理过程就变成解决每个层级的权限处理.
我们先来看看系统权限管理受到哪些因素影响: 部署方式(外网内网), 对接平台, 账户管理员购买的服务和开启 / 关闭的功能, 账户级角色(账户管理员, 子管理员), 房间角色(管理员, 讲师, 助手, 嘉宾等).
我们把每一层抽象成一个处理器, 称为 parser, 简单区分之后可以分为: deployParser(部署方式),platformParser(平台),accountParser(账户),roleParser(角色).
UNIX 中有一个 pipeline(管道)的概念, 一个程序的输出直接成为下一个程序的输入. 结合到我们的业务, 可以输入一个默认的 metaConfig, 然后依次通过各个层级的 parser 函数, 最终输出一个 metaConfig.
JS 中 pipeline 的简单实现如下, compose 函数的处理顺序是反着的, 可以查看 Redux 中的实现.
- // 管道函数
- const pipe = (...funcs) => {
- if (funcs.length === 0) {
- return arg => arg
- }
- if (funcs.length === 1) {
- return funcs[0]
- }
- return funcs.reduce((a, b) => (...args) => b(a(...args)))
- }
把我们抽象好的 parser 丢到 pipe 函数中, 结果如下:
- metaConfig = {
- ...pipe(
- deployParser,
- platformParser,
- accountParser.createAccountParser(account),
- roleParser,
- )(defaultMetaConfig)
- }
注意 accountParser.createAccountParser(account)这行, 我们下面一节分析.
这样我们通过管道函数将权限的处理拆分成多个层级, 每个层级会操作 metaConfig 内对应的属性, 是不是简单明了. 因为是函数式的处理, 可以直接放在 reducer 中计算出 metaConfig 然后保存到 Redux 中.
这里处理权限 (不仅限于权限, 还包括一些账户基础信息) 的操作分为两种情况:
直接赋值, 如账户信息.
- // 需要重置的数据
- newConfig.isSuperManager = false
- newConfig.isAdmin = true
- newConfig.name = manager.name
merge 操作, 将当前层级的权限与上一级传过来的权限进行 merge 操作, 得出权限结果传给下一级. 因为此处有一个权限限制的概念, 如果 platformParser(平台)中没有短信功能, 则 accountParser(账户)也应该没有, 在 merge 函数中使用 & 操作将该层级与上级权限 merge, 得出该权限结果传给下一级.
- accountParser = metaConfig => ({
- ...metaConfig,
- // 在 accountParser 中进行 merge 操作, 合并从上一层传来的 metaConfig, 这样的权限处理可能有多处
- somePermission: mergeSomePermission(metaConfig.somePermission),
- ...
- })
- // merge 函数内进行具体的 & 操作,
- mergeSomePermission = prePermission => {
- // 当前层级没有使用短信的权限
- prePermission.canUseMsg = prePermission & false,
- // 每个 merge 函数可以处理多个权限点, 这里只写了一个
- ...
- }
细化分层(柯里化与组合)
通过上面的分层可以从大的方向上去解决权限问题, 但是业务中的权限是动态的, 不断扩展的, 如何处理业务迭代中产生的这些问题? 比如上面例子中 mergeSomePermission 的短信权限是限定死的为 false, 但是可能有的角色有这个权限, 而其他角色没有这个权限. 在 account 这层一个简单的 parser 无法处理不同账户间之间的差异, 而且不同级别账户需要处理的权限范围可能也不一样, 同一层级还需要不同的处理函数, 用 account 作为参数, 来细化各个处理器.
我们可能需要下面的代码, 在账户权限处理中加入不同账户的处理器:
- accountParser = (account, metaConfig) => {
- cosnt { superManager = null, normalManager = null } = account
- // 分别处理 superManager 和 normalManager 的权限
- let newConfig = superManagerParser(superManager, metaConfig)
- newConfig = normalManagerParser(normalManager, newConfig)
- return newConfig
- }
- superManagerParser = (superManager = null , metaConfig) =>
- // 如果是主管理员则处理
- superManager
- ? ({
- ...metaConfig,
- // 根据 superManager 信息处理
- somePermission: mergeSomePermission(superManager, metaConfig.somePermission),
- // 主管理员功能需要多处理一些权限
- someSystemPermission: mergeSomeSystemPermission(superManager, metaConfig.somePermission)
- })
- : metaConfig
- normalManagerParser = (normalManager, metaConfig) =>
- normalManager
- ? ({
- ...metaConfig,
- // 根据 normalManager 信息处理
- somePermission: mergeSomePermission(normalManager, metaConfig.somePermission)
- })
- : metaConfig
从之前的管道处理中我们已经看到一些函数式编程的影子, 我们可以继续使用一些函数式的方法来加工上面的函数. 管道处理中的 accountParser.createAccountParser(account)就是处理这个问题的.
- // 函数柯里化
- createSuperManagerParser = (superManager = null) => metaConfig =>
- // 如果是主管理员则处理
- superManager
- ? ({
- ...metaConfig,
- // 主管理员功能需要多处理一些权限
- someSystemPermission: mergeSomeSystemPermission(superManager, metaCofig.somePermission)
- somePermission: mergeSomePermission(superManager, metaCofig.somePermission)
- })
- : metaConfig
- // 函数柯里化
- createNormalManagerParser = (normalManager = null) => metaConfig =>
- normalManager
- ? ({
- ...metaConfig,
- somePermission: mergeSomePermission(normalManager, metaCofig.somePermission)
- })
- : metaConfig
- // 合并成一个账户级的 parser
- const createAccountParser = account => {
- const { normalManger = null, super_manager = null } = account || {}
- return pipe(
- createSuperManagerParser(super_manager),
- createNormalManagerParser(normalManger),
- )
- }
我们使用柯里化将两个 parser 函数处理后, 可以使它们都接受 metaCofig 作为参数, 并继续使用一个管道组合成账户级别的 accountParser, 它的参数还是 metaConfig. 这样我们在 account 这层用柯里化和组合使得 parser 也可以用管道进行再次分层处理.
同样的操作也可以应用在角色处理器 roleParser 中. 应用 RBAC 权限管理, 一个角色对应一个 parser, 使用柯里化和 pipe 合成一个大的 roleParser.
介绍到这里, 本文所要说的函数式编程在前端权限管理中的应用就差不多了.
为什么这么处理
大致有以下几点原因:
分层解耦. 将各部分的代码分隔开, 每个层级只处理自己的部分. 代码清晰易维护, 团队其他成员也能迅速理解思路.
可组合扩展. 通过柯里化和管道, 组合, 可以实现无限分级, 即使后面权限变得更复杂, 也可以通过添加层级, 组合 parser 来应对.
整个处理过程是函数式的, 只有简单的输入输出, 对外界系统无影响, 放在 Redux 的 reducer 中真香.
总结
本文主要介绍了函数式编程 (管道, 柯里化, 组合) 在前端权限管理中的应用, 通过分层解耦, 多级分层将复杂的权限管理拆解成细粒度的 parser 函数. 水平有限, 其实也没有用的很深, 只是基本解决了现有的问题. 业务开发久了, 可能觉得没什么提升, 但是在日常的开发中也是可以活学活用的, 将一些编程的基础思想积极应用到开发中也许有意向不到的结果. 这里写出来供大家参考, 如果有更好的想法也欢迎一起讨论.
来源: https://juejin.im/post/5c1605ebf265da61620d4b3c