woxixiulayin / blog-assets Goto Github PK
View Code? Open in Web Editor NEW个人博客
个人博客
本文主要是自己在实际业务开发中的一些总结,写出来希望与大家一起探讨。
首先介绍一下业务背景:
综上,在不同部署平台下,不同级别(角色)的人员在同一个房间里,他们所看到的界面和能使用的功能是不一样的。而且一个角色受限于部署平台和主管理员所购买的平台服务,或者随着主管理员关闭/开放某些功能,看到的界面也会不一样。
所以我们需要做一套权限管理系统来综合处理这些信息(平台、账户、角色),保证各角色看到不同的界面。可以看到我们这里说的权限已经不仅限于简单的角色权限,还包括角色之上的平台和账户管理员的限制。
因为最后的权限取决于所登录的账户,所以在开发中,我们将权限和账户信息放到了一起,统称为metaConfig,即账户元信息,包含账户名字、角色等基本信息,所属主账号,具体角色信息,角色权限等,它将决定最终的界面显示。
我们使用React和Redux来开发,metaConfig对象可以直接存在Redux中进行管理。
在我们的系统中,metaConfig的处理是放在reducer中的,会有一个默认的defaultMetaConfig,通过reducer生成最后的metaConfig。权限管理最关键的就是如何生成各个角色对应的metaConfig,总结起来就是:
metaConfig=f(defaultMetaConfig)
把复杂问题拆分成简单问题
是开发中的一个重要手段。这里我们可以通过分层处理的方式,将权限管理拆分成多个层级,每层对应一个处理模块。这样一个大的权限处理过程就变成解决每个层级的权限处理。
我们先来看看系统权限管理受到哪些因素影响:部署方式(外网内网),对接平台,账户管理员购买的服务和开启/关闭的功能,账户级角色(账户管理员、子管理员),房间角色(管理员、讲师、助手、嘉宾等)。
我们把每一层抽象成一个处理器,称为parser,简单区分之后可以分为:deployParser(部署方式),platformParser(平台),accountParser(账户),roleParser(角色)。
UNIX中有一个pipeline(管道)的概念,一个程序的输出直接成为下一个程序的输入。结合到我们的业务,可以输入一个默认的metaConfig,然后依次通过各个层级的parser函数,最终输出一个metaConfig。
js中pipeline的简单实现如下,compose函数的处理顺序是反着的,可以查看Redux中compose的实现。
// 管道函数
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
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函数。水平有限,其实也没有用的很深,只是基本解决了现有的问题。业务开发久了,可能觉得没什么提升,但是在日常的开发中也是可以活学活用的,将一些编程的基础**积极应用到开发中也许有意向不到的结果。这里写出来供大家参考,如果有更好的想法也欢迎一起讨论。
原文地址:#1
接着上一篇继续介绍开发中用到的相关技术
RxJS 是使用 Observables 的响应式编程的库,它使编写异步或基于回调的代码更容易
Z-Factory中主要是在Service类内使用Observables类型的变量,当某些界面或Service依赖该变量时,可以直接监听该变量并使用Rxjs提供的函数式处理方法,也就是响应式编程
Rxjs中文网
import { Observable } from 'rxjs'
// 创建observable
var observable = Observable.create(function subscribe(observer) {
observer.next(1);
observer.next(2);
observer.next(3);
// observer.error('error');
observer.complete();
});
// 创建观察者:包含3个或部分通知类型的处理函数
var observer = {
next: x => console.log('Observer got a next value: ' + x),
error: err => console.error('Observer got an error: ' + err),
complete: () => console.log('Observer got a complete notification'),
}
// 订阅:只有subscribe后,observable内部才会执行
const subscription1 = observable.subscribe(observer)
// 清理:清除订阅方法(React组件退出时用的多)
subscription1.unsubscribe()
var button = document.querySelector('button');
// 常规事件监听
button.addEventListener('click', () => console.log('Clicked!'));
import { fromEvent } from 'rxjs'
// Rxjs创建, fromEvent: 事件对象 + 事件名称
const observable = fromEvent(button, 'click')
const callback = data => console.log(data)
// 内部将callback函数作为观察者的next方法,并忽略其他类型的通知
const subscription1 = observable.subscribe(callback)
当然使用event回调也可以实现响应回调的功能,但是它们有以下几个不同点
// 分流处理
private getServerErrorSubject(): Observable<IConsoleLog> {
return this.serverAgentService.$serverData.pipe(
filter((data: IPackage) => (data.action === ServerActions.error)),
map((data: IPackage<ErrorPayload>) => this.createLog(LogTypes.studioServer, ConsoleLogLevel.error, data.payload)),
)
}
private getServerLogSubject(): Observable<IConsoleLog> {
return this.serverAgentService.$serverData.pipe(
filter((data: IPackage) => (data.action === ServerActions.log)),
map((data: IPackage<LogPayload>) => this.createLog(LogTypes.studioServer, ConsoleLogLevel.info, data.payload))
)
}
private getRunStatusSubject(): Observable<IConsoleLog> {
return this.serverAgentService.$serverData.pipe(
filter((data: IPackage) => (data.action === ServerActions.run)),
map((data: IPackage<RunningPayLoad>) => {
const { status } = data.payload
return this.createLog(LogTypes.studioServer, ConsoleLogLevel.info, {
text: `当前程序状态:${status}`
})
})
)
}
// 合并流的结果,汇合
private bindObservable(): Subscription {
this.$serverError = this.getServerErrorSubject()
this.$serverLog = this.getServerLogSubject()
this.$runStatus = this.getRunStatusSubject()
this._logSubscription = merge(
this.$serverError,
this.$serverLog,
this.$runStatus,
this.$studioLogSubject,
).pipe(
tap(data => this.log.debug('receive console data', data)),
// 通过scan累计所有日志
scan((logList, log) => {
return [...logList, log]
}, [])
).subscribe(this.$log)
}
// 在日志窗口组件中监听
......
useEffect(() => {
const consoleLogService = useService(ConsoleLogService)
const subscription = consoleLogService.$log.subscribe(list => {
logData.current = list
// 定时器节流
setIntervalFlag(true)
if (!consolePanelIsShow) {
changeConsolePanelIsShow(true)
}
})
// 返回清理方法
return () => subscription.unsubscribe()
}, [ consolePanelIsShow, changeConsolePanelIsShow ])
由状态驱动视图的开发理念越来越普及,复杂应用的开发基本通过管理状态来控制视图。所以在应用初始阶段就应该设计好状态包括状态之间的关联和状态与UI的关系。当然大部分情况下状态管理库已经帮我们做好这些,但是一些复杂的应用还是无法满足,这个时候就需要自己去想解决方案。
整个桌面应用的状态大致包含两部分:界面状态和Service状态
所以状态到界面的传递有以下三种方法
结合一开始的软件整体结构图,整个应用的逻辑、状态、视图还是比较清晰的。其实我们的核心还是没有变,将界面与逻辑的开发分开,通过状态连接两者,将项目的层次结构分清,便于后续的迭代开发。
下一期继续介绍可视化部分的具体实现
RPA:Robotic Process Automation,即机器人流程自动化。简单来说就是通过机器人来运行制定好的流程,流程可以是有规则的复杂操作,可以代替人工进行日常运转,减小人力
RPA编辑器:我们需要一款门槛较低,用户可以迅速上手自己产出RPA脚本进行部署的开发工具,并且可以在开发过程中进行调试。RPA编辑器应运而生,简单来说它就是一款可视化的编程开发工具,通过拖拽组件的形式搭建自动化流程(RPA脚本)。
在开发过程中参考了vscode(IDE开发)、blockly(可视化编程)等开源产品,其中有很多技术方面的东西希望更大家分享一下,后面准备用三期的时间,给大家分享下相关的内容,主题就暂定为如何使用Electron开发可视化编程工具。以下是整个分享的内容。
- 中间是整个项目的层次结构
- 左边是用到的技术栈
- 右边是后台服务,提供应用的原生能力,通过websocket连接
- Electron **桌面应用程序**
- React **UI界面开发**
- Dva **状态管理与逻辑封装**
- Antd **组件库**
- inversify **依赖注入,解决IDE服务的解耦与复用**
- Rxjs **服务内部状态的管理**
- G6 **流程图编辑库**
该页面用于组织整个业务的运行流程,将大段的工作任务拆分成一个个小流程,通过流程图组织起来,中间可以通过变量传递信息。
该页面是最基础的工作页面,用于搭建具体的业务细节,通过从左侧组件库拖拽业务组件,在组件进行拼接,然后在右侧修改组件内的属性参数。
模式切换区域支持切换到源码查看与桌面模式编辑
编辑好任务后,点击运行按键,会将可视化的内容(可以是完整的流程、单个流程、单个组件)转成python源码,发送给engine执行,并将执行中的输出可结果返回给控制台显示。
- Electron可以通过前端技术来开发桌面应用程序,让开发人员关注应用的核心实现(界面与应用逻辑),而不用操心原生程序的实现
- Electron开发的程序可以跨端,意味着可以同时构建出三个平台的应用程序,甚至同一套代码在网页上运行(如果不使用node模块)
Electron运行package.json中的main脚本的进程被称为主进程。通过主进程来创建web页面展示用户界面。一个Electron应用只有一个主进程。
// 在主进程通过BrowserWindow创建页面,即渲染进程
mainWindow = new BrowserWindow({
width: 765,
height: 495,
resizable: false,
webPreferences: {
webSecurity: false
},
frame: false
});
// 加载页面,webFile是URL,可以是本地html也可以是网络上的
// Z-Factory中webFile是本地的index.html文件地址
mainWindow.loadURL(webFile);
- 在Z-Factory中主界面是一个单独的页面,登录页、项目管理页和编辑页面是根据不同的状态调整布局尺寸实现的。
- 在桌面模式下,独立的模态框是一个个单独的页面,用一个独立的页面去渲染。
打包分为两步
由前面的简介知道Electron分为主进程和渲染进程,所以js部分的内容也分为主进程和渲染进程,分别配置独立的webpack进行打包。将打包后的文件放入dist文件夹。最后统一打包收入程序的特定目录下。当程序运行后,运行主进程js文件,主进程通过BrowserWindow加载index.html页面,页面加载后会下载对应的渲染进程js代码,最后呈现出主界面。
因为打包过程比较繁琐,一般采用现成的集成打包工具, Z-Factory使用electron-builder进行打包。通过配置文件可以设置应用程序的名称、图标、32位/64位,win/mac等
// 文件路径 build/electron-config.yml
appId: Z-Factory
productName: Z-Factory // 应用名称
directories: // 资源文件夹
buildResources: resources
output: release
// 包含的文件
files:
- dist/
- index.html
- modal.html
- resources/
- dist/main.prod.js
- dist/main.prod.js.map
- package.json
win:
target:
- target: 'nsis' // 打包成安装包
arch:
- 'ia32' // 32位
asar: false
nsis:
oneClick: false
perMachine: true
runAfterFinish: false
allowToChangeInstallationDirectory: true
为什么加入CI
如何实现
在jenkins上配置上述打包流程
该CI流程可以复用到Z-Bot上,基本实现推送代码后,拉取最新的engine进行编译并上传到对应的发布地址上
Z-Factory除了应用界面,还有很多纯逻辑的代码,比如与engine的通信、文件处理、项目文件管理、组件管理、组件转代码等。
需要一个比较好的方式组织这些代码,并且满足这些特点。
基于以上几个特点,使用类的方式来定义一个逻辑模块,提供类的方法供外部使用,通过变量保存运行时的变量,如果外部需要响应变量的变化,可以定义为Observable类型(Rxjs的内容,后面会讲到)。
上面通过Service类解决了逻辑的封装的问题,但是还需要一种方式解决逻辑间的依赖调用问题。Z-Factory通过引入依赖注入框架Inversify来解决逻辑间的依赖问题。
依赖注入是一种编码技巧,多用于面向对象编程中。简单来说就是:不通过new()的方式在类内部创建依赖的对象,而是将依赖的对象在外部创建好之后,通过构造函数、函数参数等方式传递(或注入)给类使用。不需要在类内部主动申明创建,而是在程序运行的时候从外部注入。将当前对象与依赖对象解耦。
一个项目中可能使用几十,上百个类,如果人为控制对象的创建和依赖注入会变得非常复杂,容易出错。我们可以通过依赖注入框架,申明依赖间的关系,将依赖的创建、注入和生命周期管理交给框架处理。
// Z-Factory中的injector.js封装了Inversify提供的依赖注入功能,文件路径:src/base/common/injector.js
// 提供了下面几个方法
export {
container, // 依赖实例容器,map的形式保存所有被依赖的单例
useService, // 主动创建单例
injectable, // 可以被依赖的类,一般在被依赖时,会在container内创建该类的实例,并赋值给客户端的内部变量
inject, // 在类内部申明依赖
}
---------------------
// 一个典型的应用
// 通过装饰器的方式申明依赖
@injectable() // 申明可以被其他类依赖
class SocketService extends Service {
@inject() LogService: LogService // 申明依赖的对象,如果container中没有该单例,则立即创建
.....
}
const socketService = useService(SocketService) // 主动获取单例,如果没有则创建
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.