js类管理工具mana_syringe

441 阅读10分钟

你现在还用class吗?其实很多人已经很少用了,react和vue都改为函数式组件了,但是依然有框架库在用例如xflow,下面看下xflow管理class的工具mana_syringe。

这个库不是底层库,是参考了TSyringe,并基于inversifyJS封装的,本文只探索mana_syringe。

理论

mana_syringe是控制反转(定义看这里)设计模式的实现,和控制反转同时出现的还有依赖注入,依赖注入是控制反转的实现过程,特点如下:

优点:降低耦合度、提高可维护性和可扩展性、支持单元测试、提高代码重用性。

缺点:学习成本较高、增加系统复杂性、性能开销。

其实对于一个class方式构建的项目中,大量的class的管理是很复杂的,耦合多,有的还要单独写单例模式,单元测试不方便等等,而控制反转能解决这个问题。对于一个react或者vue项目来说,如果是通过class创建的组件,那么控制反转的优点不是很明显,因为类组件之间的关系比较单一-父子组件,所以inversifyJS这个库并不是很知名,但是对于一些基建框架类的项目中,用这个管理class还是很好用的。

下面就简单了解下mana_syringe的核心概念,中文也称为魔法注射器。

Container

顾名思义就是容器,容器是控制反转的核心,容器基本作用有存储和获取。

存储

有四种类型的数据可以存储,分别是constantValue、dynamicValue、class、factory。

constantValue就是普通值,而dynamicValue则除了普通值还包括异步的值和函数,而class是存储的js类,factory存储的是js函数,而这个方法能接收一个context参数,这个参数能访问到container,继而能访问其它实例,也就这个方法回返回一组实例,所以它叫factory。

获取

获取一种是通过Container上的get方法,一种是通过装饰器Inject。装饰器其实最后也是访问的get方法,这里是装饰器的原理

xflow中的使用

xflow中充斥着控制反转,这里截取一段来分析下xflow中是如何使用mana_syringe的。

image.png container.load和Module都属于mana_syringe自己制定的,Module会接收一个参数回调这里称它callback1,container.load方法会获取到callback1,然后把container.register作为参数传给callback1,可以看到callback1中有两行代码。

image.png 第一行是注册扩展点,我理解扩展点就是注册一个集合,集合内包含所有别名或者名称为IFrontendApplicationContribution的class。注册集合的方法看下图。

image.png 也就是graph-command模块所导出的类即可以通过IFrontendApplicationContribution获取也可以通过IGraphCommandService获取。

第二行是注册类,这俩本身没关系,我一开始以为这两行类似于js的函数柯里化,第一行是注册一个“IFrontendApplicationContribution”的标示,第二行以及后面的注册都是在这个标示下,看完mana_syringe源码后发现理解错了,第一行就是如上所述的意思,但第二行只是注册类。注册的类FrontendApplication中有下面一句代码。

image.png 这个代码的意思就是把所有别名为“IFrontendApplicationContribution”的类的示例都存到contributions,而别名为“IFrontendApplicationContribution”也有类似的代码,这样就形成了一个树形结构。所以在FrontendApplication类的示例中能访问到所有“IFrontendApplicationContribution”集合以及子集合的数据。

总结

可以通过container.register把一个class单独注册到container中,也可以通过Contribution.register和contri(别名)把类注册到集合中,这样就能一次获取整个集合的实例。

继续深挖

既然都看到这了,不看看单独注册和集合注册的到底是如何注册到container的总感觉缺了点什么。

看源码前因为mana_syringe是基于InversifyJs的,所以最终肯定是要注册到InversifyJs的container中的。

单独注册

单独注册的方式如上面的“register(FrontendApplication)”,源码中container.register的定义是多态的,可以把token和options分别,也可以一起传递。单独注册的时候就是单独传递的token,但是我们在上面的代码中看到container.register传递的是FrontendApplication,但是这里命名为token,难道class可以作为token?,看下InversifyJs中token的定义显然是"没有class的(有class的)"。原谅我浅陋的ts知识,Newable就是指的class,所以token可以是class。

image.png

image.png Register是注册器模块,用来解析要注册的配置包括token和option以及数据绑定。

其实看inversifyJS的文档可以知道要让一个class能被管理需要两步:bound(注册)和声明。

bound是调用的"container.bind",这里完成把标示和数据存储到container中。

而声明则是用的"@injectable"装饰器,声明得意思是表明当前这个class已经注入到容器中了。但是xflow中并没有使用injectable,而是用了"@singleton"和”@transient“,这俩其实是mana_syringe封装的装饰器,就是加上了inversifyJs的lifecycle选项。bound就用的container.register。

通过xflow来看container.register其实就两种使用场景,单独注册和集合注册,单独注册就是一个标示注册一个数据,而集合注册则是一个标示注册一组数据。

单独注册会调用Register的resolveTarget函数,Register模块比较重要,分析下整个模块。

image.png 这里先只分析单独注册涉及的逻辑。

  1. resolveTarget

顾名思义,解析target,target就是register的class,mana_syringe会把所有的需要管理的数据都保存到option中,class保存到useClass,value保存到useValue,动态value保存到useDynamicValue,factory保存到useFactory中,token用的是class,接下来来就是解析option了。

  1. resolveOption

解析option主要是加上option的默认值,目前就一个生命周期默认值是”transient“,非单例。然后就是新建Register对象,在新的Register对象上执行resolve方法。

  1. resolve

resolve是一个通用函数,所以会区分mono和multiple,multiple就是下面要说的集合注册,mono就是现在要说的单独注册。可以看到单独注册的逻辑里还有一段contrb的代码,这是跟集合注册有关的,集合注册再分析这里,这个很重要,涉及集合注册的核心,继续看单独注册。

  1. resolveMono

这里也是职责很明确,把option中的useValue、useDynamic、useClass、useFactory都依次传递给bindMonoToken,bindMonoToken内部就是调用的inversifyJs的bind方法绑定标识,这里指的是class,前面说了class可以作为token标识,然后把option中useValue、useDynamic、useClass、useFactory分别传给toConstantValue、toDynamicValue、toFactory、to,至此就完成了单独注册的绑定了。

总结

从上面的分析可以看出单独注册比较简单,就说FrontendApplication类的话,其实就是把这个类作为标识以及被管理的class传给了inversifyJs。

集合注册

这个是重头戏,也是封装的逻辑比较多的地方,所以我把分析的过程从头到脚完整的记录下来。

从使用来看,涉及到以下三个部分声明、绑定、使用。

  1. 装饰器声明 image.png contrib中的两个元素都是标识,第一个是集合标识,第二个是单独获取class的标识,singleton会处理option然后传给inversifyJS的injectable。下面看下处理逻辑。

image.png singleton和transient只是生命周期不同,而injectable函数会把contrib对象通过Reflect.defineMetadata存入class中,然后会交给inversify的injectable,然后完成了声明的部分。 2. 绑定集合标识

image.png

在单独注册中其实已经涉及了Module,不过秉承着由浅入深的习惯,把Module的分析放在这里,读完集合注册的过程其实就完整的了解了过程。

  • a. Module

image.png 调用Module最终是实例化SyringeModule,把Module的参数(简称callback1)存入baseRegister。

  • b. load appMainModule

container的load方法会处理appMainModule。

image.png module.register最终就是执行的callback1,所以callback1函数中的参数register指的是container.register。

  • c. Contribution.register

image.png Contribution.register其实最终执行的换是container.register,而identifier就是集合标识,通过useDynamic可以看出集合标识绑定到了这个动态值上,动态值的逻辑看下面获取集合的分析,这里继续看集合绑定的过程。

兜兜转转又回到了container.register,不过此时调用的是Registry.resolveOption。在单独注册那已经分析过这个函数了,继续往下看再次执行到了Registry.resolve,这里要再分析下。

image.png registry.named(this.name)指的是集合标识,而token是一个Symbol,集合中所有类都用这个Symbol。

不难看出集合注册对应的是resolveMultiple,但是我把单独注册的一段逻辑也圈起来了而且要先分析因为这里涉及到了集合的声明。

前面说过集合声明的时候有个contrib选项,当时说contrib中包含类对于集合的标识以及自己本身的标识,而上图中的代码就是类对于集合的标识是如何绑定的。

先看下register.options.contrib是如何从声明中读出来的。

image.png 通过defineMetadata存储的,当然也要通过getMetadata获取,获取后直接赋给option了,option就获取到了声明的contrib。

接下来是把集合标识以及类本身标识绑定到container中接着调用了toService方法,toService的作用是一个标识可以绑定多个class,所以多个class都声明了IFrontendApplicationContribution标识,那通过这个标识就能获取所有的声明它得class,所以我把它称为集合。

现在多个class都绑定到一个集合,下面思路再回到register.resolveMultiple。

image.png

image.png 和单独注册类似都是把标识和被管理的对象都绑定到inversify的container中,但是注意这里的dynamicList是一个数组,数组中的元素是一个函数,对于集合而言这个函数的作用就是存储集合的标示,然后提供返回集合中数据的方法。

image.png 不过这里多了个bindName的过程,这个是干什么的?

image.png 这个whenTargetNamed又是什么?这里需要看下inversify的文档了,因为这里是inversify的API。

image.png 看文档开头就描述的很清晰了,如果出现了一个标识绑定了多个被管理对象怎么办?答案是通过whenTargetNamed区分,通过whenTargetNamed相当于给被管理对象又加个区分标识,获取时在@inject标识后再@named就能获取具体的被管理对象了,绑定集合到此完结,下面分析使用。

  1. 获取集合

接着上面的@named继续分析,xflow没用@named啊,而通过代码推断是通过如下方式获取的。

image.png 看下contrib这个装饰器的定义。

image.png 是的,单看名称就知道@contrib其实是@inject和@named组合,token就是集合标识而Provider是一个symbol值,看下图就明白了。

image.png 在绑定的集合时候的这个token即使@inject的参数,而集合标识就是@named参数所以@contrib(IFrontendApplicationContribution)就能获取到DefaultContributionProvider实例。

image.png 所以上图中this.contributions.getContributions()就是DefaultContributionProvider的getContributions方法。

image.png 所以最终是通过集合标识serviceIdentifier调用currentContainer.getAll获取到集合中所有的类。

总结

集合注册主要是用了inversify的whenTargetNamed、named这俩API,而集合通用标识的Contribution.Provider和单独注册时的绑定一样,whenTargetNamed对集合通用标识添加了named的区分也就是每个集合的标识,而@named和@inject封装到@contrib中确实更易用了,最后通过每个集合的标识获取到了DefaultContributionProvider实例,然后在这个实例中再通过集合标识从容器中获取集合中所有的类。至此、完结。