Giter VIP home page Giter VIP logo

blog-assets's People

Contributors

woxixiulayin avatar

Stargazers

 avatar

Watchers

 avatar  avatar

blog-assets's Issues

函数式编程在前端权限管理中的应用

函数式编程在前端权限管理中的应用

解决什么问题

本文主要是自己在实际业务开发中的一些总结,写出来希望与大家一起探讨。

首先介绍一下业务背景:

  • 我们开发的是一套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中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
  • 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函数。水平有限,其实也没有用的很深,只是基本解决了现有的问题。业务开发久了,可能觉得没什么提升,但是在日常的开发中也是可以活学活用的,将一些编程的基础**积极应用到开发中也许有意向不到的结果。这里写出来供大家参考,如果有更好的想法也欢迎一起讨论。

原文地址:#1

如何使用Electron开发可视化编程工具(二)

技术背景介绍和选型思考

接着上一篇继续介绍开发中用到的相关技术

Rxjs的使用

RxJS 是使用 Observables 的响应式编程的库,它使编写异步或基于回调的代码更容易
Z-Factory中主要是在Service类内使用Observables类型的变量,当某些界面或Service依赖该变量时,可以直接监听该变量并使用Rxjs提供的函数式处理方法,也就是响应式编程
Rxjs中文网

理清Rxjs中的几个概念

  • Observable(可观察对象)
    • 一个数据类型,表示一个可调用的未来值或事件的集合,可以通过其上的observable.subscribe方法监听这些值
    • Observables 是使用 Rx.Observable.create 或创建操作符创建的,并使用观察者来订阅它,然后执行它并发送 next / error / complete 通知给观察者,而且执行可能会被清理
    • 还可以通过操作符生成Observable(比较常用):of, from,fromEvent
    • 4个步骤:创建、订阅、执行、清理
  • Observer(观察者)
    • 一组回调函数的集合,每个回调函数对应一种Observable发送的通知类型
    • 也可以传递部分回调函数或者,则其他类型的通知会被会略掉
    • 如果只在observable.subscribe中传递一个回调函数,则observable.subscribe内部会创建一个观察者,并把回调函数作为next的处理方法
  • Subscription (订阅)
    • observable是惰性的,意味着只有在subscribe之后,才会执行
  • Operators(操作符)
    • 函数式编程风格的纯函数:map、filter、scan、debounce和throttle等
    • 处理Observable推送的值
  • 主体
    • 与observable的区别,惰性执行和单一来源
  • 调度器
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回调也可以实现响应回调的功能,但是它们有以下几个不同点

  • event需要申明事件类型,Rxjs中只需要申明Observables类型的变量
  • 事件名称不好管理,我们只需直接引用Observables类型的变量
  • 有丰富的函数式操作符,比如filter、merge,可以监听一个源,然后做过滤分发,在与engine的通信中用的比较多
  • 支持链式调用,可以实现流式处理,可以对一个源头的数据进行不同的分流分类处理,比如与engine的通信,底层用一个响应式数据接收数据,业务上可能有不同的处理方式,可以接一个源头数据,进行分流分类处理
  • 监听BehaviorSubject时,可以直接获得上次的状态,而event不行,只能事件触发时才有数据,界面监听数据时,必须一开始就有数据,大部分状态都是基于这个原因使用Rxjs管理状态
  • Rxjs可以基于数组、promise、类事件、单值等创建可观察数据
// 分流处理
 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状态

  • 界面状态
    • 纯界面端的状态好说,直接由redux开道,通过Dva将界面的model分类,封装model的state、reducer、effects
  • Service状态(内部变量)
    • 我们有大量Service类内需要提供给界面的状态,这部分状态可以直接在model中包装然后提供给界面,但是有几点不太好:
      • 需要多余的样板代码去封装service的内容来生成model,然后connect到组件
      • 有些状态的功能比较专一,不需要共享,只需要一对一传给界面
      • redux内state的变化会触发更新,多余的变化不利于性能
      • 其实Dva中model的概念就包含了状态和状态关系的处理的内容,service也包含这样的能力,所以没有必要绕一层Dva去联通组件,就像Angular中的service包含了逻辑与状态处理
    • 所以我们使用Rxjs定义可观察数据,将需要的数据传递给redux或者直接传给组件,当然这样会失去一些统一性,但更有效率

所以状态到界面的传递有以下三种方法

  • redux ---> ui
  • service ---> redux ---> ui
  • service ---> ui

结合一开始的软件整体结构图,整个应用的逻辑、状态、视图还是比较清晰的。其实我们的核心还是没有变,将界面与逻辑的开发分开,通过状态连接两者,将项目的层次结构分清,便于后续的迭代开发。

下一期继续介绍可视化部分的具体实现

如何使用Electron开发可视化编程工具(一)

分享内容

  • 背景和目标
  • 技术方案
    • 整体技术方案
    • 整体功能介绍
  • 技术背景介绍和选型思考
    • Electron
      • 打包与CI
    • service类与依赖注入
    • Rxjs的使用
    • 状态管理
      • redux
      • service
      • redux、service和ui层的关系
  • 具体业务实现
    • 可视化与代码生成
      • 流程图
        • 拖拽实现
        • 代码生成
      • 任务编辑
        • 拖拽实现
        • 代码生成
        • 编辑操作
        • 组件管理
    • 项目管理
    • engine通信
    • 与engine间的原生交互
  • 总结技术收获与不足

分享计划

  • 一期:技术方案与工程化+技术背景选型思考
  • 二期:可视化代码生成
  • 三期:项目管理和engine通信相关内容 + 总结

背景和目标

背景

RPA:Robotic Process Automation,即机器人流程自动化。简单来说就是通过机器人来运行制定好的流程,流程可以是有规则的复杂操作,可以代替人工进行日常运转,减小人力
RPA编辑器:我们需要一款门槛较低,用户可以迅速上手自己产出RPA脚本进行部署的开发工具,并且可以在开发过程中进行调试。RPA编辑器应运而生,简单来说它就是一款可视化的编程开发工具,通过拖拽组件的形式搭建自动化流程(RPA脚本)。
在开发过程中参考了vscode(IDE开发)、blockly(可视化编程)等开源产品,其中有很多技术方面的东西希望更大家分享一下,后面准备用三期的时间,给大家分享下相关的内容,主题就暂定为如何使用Electron开发可视化编程工具。以下是整个分享的内容。

技术方案与工程化

整体技术方案

先放一张项目的整体结构

factory整体技术架构

- 中间是整个项目的层次结构
- 左边是用到的技术栈
- 右边是后台服务,提供应用的原生能力,通过websocket连接

项目用到的技术栈如下

- Electron **桌面应用程序**
- React  **UI界面开发**
- Dva  **状态管理与逻辑封装**
- Antd  **组件库**
- inversify **依赖注入,解决IDE服务的解耦与复用**
- Rxjs  **服务内部状态的管理**
- G6 **流程图编辑库**

整体功能介绍

首页loading与登录页

  • loading: 为了避免白屏,在主进程加载的index页面上写了静态页面,后续js加载完毕后渲染login覆盖
  • 登录页:联网登录和后门模式 + 秘钥激活(RSA加密)

项目管理页面(模板与历史文件)

  • 模板项目:第一次打开应用时从resource中复制到文档下
  • 历史文件:有单独的服务记录修改历史记录并保存到对应的用户信息文件中

流程编辑页面

该页面用于组织整个业务的运行流程,将大段的工作任务拆分成一个个小流程,通过流程图组织起来,中间可以通过变量传递信息。

流程编辑页面

任务编辑页面

该页面是最基础的工作页面,用于搭建具体的业务细节,通过从左侧组件库拖拽业务组件,在组件进行拼接,然后在右侧修改组件内的属性参数。
模式切换区域支持切换到源码查看与桌面模式编辑

任务编辑页面

运行

编辑好任务后,点击运行按键,会将可视化的内容(可以是完整的流程、单个流程、单个组件)转成python源码,发送给engine执行,并将执行中的输出可结果返回给控制台显示。

技术背景介绍和选型思考


Electron

简单介绍

- 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);

渲染进程

  • 一个页面就是一个渲染进程。与浏览器一样,Electron也是一个多进程应用,一个页面(标签页)就是一个进程,进程间相互独立。一个页面包含多个线程(GUI渲染线程、js引擎线程等)
  • 渲染进程与主进程间可以通过Electron提供的remote模块进行通信,比如需要在不同页面间传递数据或者调用主进程的功能。remote提供的通信方式使用起来跟event一样方便。
  • Eletron同时对主进程和渲染进程暴露了Node.js的所有接口,这意味着可以在Electron的web页面调用Node的API,比如fs和path。
- 在Z-Factory中主界面是一个单独的页面,登录页、项目管理页和编辑页面是根据不同的状态调整布局尺寸实现的。
- 在桌面模式下,独立的模态框是一个个单独的页面,用一个独立的页面去渲染。

打包

  • 打包分为两步

    • 编译压缩主进程和渲染进程的js代码
    • 使用工具打包Electron应用,生成应用的安装程序
  • 由前面的简介知道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

  • 为什么加入CI

    • 程序的打包过程如下:
      • 下载外部资源(engine) --> 编译js文件 --> 打包Electron应用 --> 上传应用程序
    • 整个打包过程持续5-10分钟,并且特别耗费系统资源,各环节容易出错,加入CI自动打包发布可以很好的提高效率
  • 如何实现

    • 在jenkins上配置上述打包流程

      • node脚本实现下载外部资源: yarn run download-engine
      • 编译js文件: yarn run build
        • 这一步在jenkins的容器中进行,因为可以利用之前下载的node_modules文件,免去重复下载
      • 打包Electron应用: yarn run package
        • 这一步放在docker中进行,原始镜像为electronuserland/builder:wine,提供了打包window所需的环境
        • 启动docker时,将项目目录挂载到docker指定目录下
      • 上传应用程序:yarn run upload-exe
    • 该CI流程可以复用到Z-Bot上,基本实现推送代码后,拉取最新的engine进行编译并上传到对应的发布地址上

Service与依赖注入(DI-Dependency Injection)

Service类

Z-Factory除了应用界面,还有很多纯逻辑的代码,比如与engine的通信、文件处理、项目文件管理、组件管理、组件转代码等。
需要一个比较好的方式组织这些代码,并且满足这些特点。

  • 将逻辑与逻辑需要的状态封装到一起
  • 逻辑需要隔离复用
  • 不需要人为来管理这些逻辑模块,要简单方便

基于以上几个特点,使用类的方式来定义一个逻辑模块,提供类的方法供外部使用,通过变量保存运行时的变量,如果外部需要响应变量的变化,可以定义为Observable类型(Rxjs的内容,后面会讲到)。
上面通过Service类解决了逻辑的封装的问题,但是还需要一种方式解决逻辑间的依赖调用问题。Z-Factory通过引入依赖注入框架Inversify来解决逻辑间的依赖问题。

什么是依赖注入

依赖注入是一种编码技巧,多用于面向对象编程中。简单来说就是:不通过new()的方式在类内部创建依赖的对象,而是将依赖的对象在外部创建好之后,通过构造函数、函数参数等方式传递(或注入)给类使用。不需要在类内部主动申明创建,而是在程序运行的时候从外部注入。将当前对象与依赖对象解耦。

  • 这样有几个好处
    • 将程序解耦,不同的业务逻辑封装在不同的service内
    • 提高程序的扩展性,因为没有绑定依赖,依赖的实例可以在运行时确定,可以替换成具有相同接口的类

什么是依赖注入框架

一个项目中可能使用几十,上百个类,如果人为控制对象的创建和依赖注入会变得非常复杂,容易出错。我们可以通过依赖注入框架,申明依赖间的关系,将依赖的创建、注入和生命周期管理交给框架处理。

// 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) // 主动获取单例,如果没有则创建

如何使用Electron开发可视化编程工具(二)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.