- Notifications
You must be signed in to change notification settings - Fork2
Core of rockerjs, the solution for Node EE. Rockerjs-core provides a IoC container and AOP based on TypeScript, Compatible with any system or framework.
License
weidian-inc/rockerjs-core
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
基于 TypeScript 和注解的轻量级IoC容器,提供了依赖注入、面向切面编程及异常处理等功能。Rockerjs Core可在任意工程中引入,是一个框架无关的IoC容器。
@rockerjs/core模块不依赖于任何框架,并与现有框架、库、类等保持兼容。通过DI(Dependency Injection)实现代码解耦和依赖解耦,在构建复杂应用时保证可扩展性与灵活性;同时提供二维编程的能力,基于注解可在各个连接点(Advice)进行非核心业务的操作,减少代码冗余;最后,它提供一种基于注解配置的简易异常处理机制 --Clamp机制,通过特定规则匹配异常处理程序实现处理。
安装
npm install @rockerjs/core
@rockerjs/core最佳实践需要结合TypeScript的装饰器一起使用(也可使用接口),因此需要在项目根目录添加tsconfig.json 文件,并配置编译选项 “experimentalDecorators”和“emitDecoratorMetadata”为 true,并推荐编译为ES6语法
示例 1
import{Container,Inject}from'@rockerjs/core';classUser{id:string="testId";name:string="testName";}classUserService{getUser(_id:string):User{returnnewUser();}}@InjectclassControlDefault{ @InjectuserService:UserService;test(){letuser:User=this.userService.getUser("test");console.log(user);}}@Inject('controllor-with-args',newDate())classControlDefaultWithArgs{name:string;time:Date;constructor(name:string,time:Date){this.name=name;this.time=time;} @InjectuserService:UserService;test(){letuser:User=this.userService.getUser("test");console.log(user,this.name,this.time);}}@Inject('controllor1','util',newDate())classControl{name:string;time:Date;constructor(name:string,time:Date){this.name=name;this.time=time;} @InjectuserService:UserService;test(){letuser:User=this.userService.getUser("test");console.log(user,this.name,this.time);}}// 通过getObject接口从容器中获取实例,参数为“单例的名称”(默认名称为类名首字母小写)Container.getObject<ControlDefault>('controlDefault').test();// 通过getObject接口从容器中获取实例,此例中并未提供实例名Container.getObject<ControlDefaultWithArgs>('controlDefaultWithArgs').test();// 通过getObject接口从容器中获取实例,此例中提供了3个参数,@rockerjs/core 认为第一个参数为实例名,剩下的参数则用于实例化Container.getObject<Control>('controllor1').test();
示例 2 : RPC
import{Container,Inject}from'@rockerjs/core';//PRC Demo实现letRPC={config:function(cfg:{serviceUrl:string,interfaces:Function[]}){if(cfg.interfaces){cfg.interfaces.forEach((type:FunctionConstructor)=>{if(type.prototype){letnewObj={},proto=type.prototype;letnms=Object.getOwnPropertyNames(proto);if(nms){nms.forEach((nm)=>{if(nm!='constructor'&&typeof(proto[nm])==='function'){newObj[nm]=function(){//{nm:方法名,arguments:参数表},改为调用远程请求过程returnarguments[0];//test return}}})}Container.provides([type,()=>{returnnewObj;}])}})}}}//--DEMO--------------------------------------------------------//1. 接口声明(注意,此处只能使用Concrete class)classProduct{getById(id:string):string{returnnull;}}//2. 应用RPC FrameworkRPC.config({serviceUrl:null,interfaces:[Product]//提供接口描述,在RPC中构建factory})//3. Service class@InjectclassService{ @Injectproduct:Product;test(){letid:string='tid';letrst=this.product.getById(id);console.log(rst);}}//4.测试Container.getObject<Service>('service').test();
提供了注解@Inject
来实现依赖的注入,当我们有如下GetDubboData
类时
classGetDubboData{p0:number;constructor(p0:number,p1:string){this.p0=p0;}}
我们可以通过以下方式实例化这个类,同时传入指定的参数
直接传递构造函数的参数
classSomeControl{ @Inject(1,'aaa')privatedubbo:GetDubboData}
给出构造函数的工厂函数
classSomeControl{ @Inject(function(){return[1,'aaa']})privatedubbo:GetDubboData}
无构造函数或参数为空
classSomeControl{ @Injectprivatedubbo:GetDubboData}
默认的实例化方法可以满足开发者的大部分需求,Rockerjs Core 提供了 provides 方法自定义实例化工厂,同时提供了获取类和类实例化函数映射表的方法。
直接传入类或工厂函数
// 形式一:形如 Container.provides(class extends UserService{})Container.provides(classextendsUserService{getUser(id:string):User{console.log(1);returnnewUser();}});
传入类及类的工厂函数
// 形式二:形如 Container.provides([UserService,FactoryFunction])Container.provides([UserService,()=>{returnnewclassextendsUserService{getUser(id:string):User{console.log(2);returnnewUser();}}();}]);
返回一个构造函数-工厂方法映射表, 结构如下
constglobalGeneralProviders:Map<FunctionConstructor,Function>=newMap<FunctionConstructor,Function>();
Container.injectClazzManually
方法提供了直接实例化注册表中的类的功能,参数为构造函数以及想要传入的参数
classSomeControl{transGet:GetTransData=Container.injectClazzManually(GetTransData,1,2);asyncgetProduct(_productId?:number){letjson:any=awaitthis.transGet.getDetail(_productId);console.log(json);}}
假设我们有一个获取异步数据的抽象类
abstractclassGetTransData{p0:numberconstructor(p0:number,p1:string){console.log(p0+p1)this.p0=p0}abstractasyncgetDetail(_proId:number):Promise<string>;}
可以通过 Container 的provides
API 来指定对应类型的工厂函数
Container.provides([GetTransData,(_p0,_p1)=>{returnnewclassextendsGetTransData{constructor(p0:number,p1:string){super(p0,p1);}asyncgetDetail(_id:number):Promise<string>{await((ms)=>newPromise(res=>setTimeout(res,ms)))(100)return`Hello${this.p0}`}}(_p0,_p1);}]);
最终通过@Inject
方法注入在测试类里面实例化这个对象
@InjectclassSomeControl{ @Inject(666,2)transGet:GetTransData;asyncgetProduct(_productId?:number){letjson:any=awaitthis.transGet.getDetail(_productId);console.log(json);}}Container.getObject<SomeControl>('someControl').getProduct();
得到输出结果
668Hello 666
面向切面编程(AOP是Aspect Oriented Program的首字母缩写)是指在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想。Rockerjs Core 提供了 AOP 编程能力
假如我们想在下面的foo
方法执行前后打点
classTest{foo(){console.log('foo')}}newTest().foo()
我们可以声明一个日志类,通过@Aspect
注解声明其为一个切面类,通过@Pointcut
注解配置想要匹配的类、方法以及需要执行的钩子, 最后通过@Before
和@After
等注解标识被修饰方法在处于对应生命周期时需要执行的方法
import{Aspect,Pointcut,Before,After}from"@rockerjs/core";@AspectclassLogger{// 必须在静态方法上注册切点 @Pointcut({clazz:Test,// 定位被修饰的类scope:"prototype",// 默认只针对实例方法做Advice,配置“static”可针对静态方法Advicerules:".*foo.*",// 通过正则匹配到对应的方法,不填则匹配所有函数advices:["before:printStart","after"]// 过滤将要执行的钩子 (可细致到函数名)})staticmain(){}// 在执行被打点方法前执行的方法 @BeforeprintStart(){console.log("log:start:",newDate());}// 可以指定多个方法 @BeforeprintStart2(){console.log("log:start:",newDate());}// 在执行被打点方法后执行的方法 @AfterprintEnd(){console.log("log:end:",newDate());}}
必须在切面类的静态方法上注册切点
Rockerjs Core 提供了Before
,After
,After_Throwing
,After_Returning
,Around
等生命周期
- Before:在被打点函数执行前执行
- After:在被打点函数执行后执行
- After_Throwing:在被打点函数抛出异常时执行
- After_Returning:在被打点函数返回结果后执行
- Around:在被打点函数执行前后执行,类似于 koa 中间件
- 在 after 后执行
- 如果原生函数没有 return 任何东西则不执行
- 可以修改返回结果
@After_ReturningprintReturn(ctx,result){// ctx 为函数执行上下文// result 为函数执行的结果result+=666returnresult}
@After_Throwingprintthrow(ctx,ex){// ctx 为函数执行上下文// ex 为错误信息console.log('Loggy catch: '+ex.message);console.log(ctx)}
@AroundcurrentTime2(ctx,next){// ctx 为函数执行上下文// next 为匹配到的函数console.log('before',Date.now());letret=next();console.log('after',Date.now(),ret);returnret;}
我们可以为某个类同时注册多个切面类,再通过composeAspects
方法将它们组合起来,默认按照声明的顺序来包裹被打点的函数,最后声明的类会包裹在最外面一层
@Aspect()classLogger{// ...}@Aspect()classLogger2{ @Pointcut({clazz:Test,advices:["before","after","around",'after_returning']})staticmain(){} @BeforeprintStart(){console.log("2:start");} @Afterprintafter(){console.log("2:after");} @After_ReturningprintReturn(ctx,result){console.log('2:after_returning',result)returnresult+2} @AroundprintAround2(ctx,next){console.log("2:around:before");letret=next();console.log("2:around:after",ret);returnret;}}@Aspect()classLogger3{// ...}composeAspects({clazz:Test,// rules: '.*foo.*',aspects:[Logger,Logger2,Logger3]});
执行结果如下:
3:start2:start1:start3:around:before2:around:before1:around:beforefoo1:around:after bar2:around:after bar3:around:after bar1:after2:after3:after1:after_returning bar2:after_returning bar3:after_returning bar
如果想自定义切面之间执行的顺序,可以在切面注解上传入切面的次序(数值小的在洋葱模型的外层):
@Aspect({order:2})classLogger{}@Aspect({order:1})classLogger2{}@Aspect({order:3})classLogger3{}composeAspects({clazz:Test,aspects:[Logger,Logger2,Logger3]});
执行顺序如下:
2:start1:start3:start2:around:before1:around:before3:around:beforefoo3:around:after bar1:around:after bar2:around:after bar3:end1:end2:end
除了通过 Rockerjs Core AOP 中的 @After_Throwing
注解来实现错误捕获,我们还提供了更简便的实现错误捕获的方法,如下例,我们先声明一个错误捕获夹,然后在被包裹的函数上使用这个错误捕获夹,当函数执行过程中有异常发生时,我们能在捕获夹的 catch 方法中拿到错误信息以及函数执行的上下文。
import{Container,Inject,Catch,Clamp,ExceptionClamp}from"@rockerjs/core";// 1. 声明一个捕获器,实现 catch 方法@ClampclassClamperextendsExceptionClamp{catch(ex,ctx){console.log("hahaha: ****",ex,ctx);}}@InjectclassTest{// 2. 使用捕获器 @Catch("Clamper")test(){thrownewError("12322");}}Container.getObject<Test>('test').test();
与@After_Throwing
同时使用时,@Catch
会先捕获到错误,再次将错误抛出,@After_Throwing
才捕获到错误
@ClampclassClamperextendsExceptionClamp{catch(ex,ctx){console.log("hahaha: ****",ex,ctx);throwex// 将错误二次抛出}}@InjectclassTest{ @Catch("Clamper")test(){thrownewError("12322");}}@AspectclassExceptionClamp2{ @Pointcut({clazz:Test,advices:['after_throwing']})staticmain(){} @After_ThrowingprintThrow(ctx,ex){console.log('Loggy catch: '+ex.message);console.log(ctx)}}Container.getObject<Test>('test').test();
请参考Contribute Guide 后提交 Pull Request。
MIT