面向庞杂运用,Node.js中的IoC容器 -- Rockerjs/core

Rockerjs Core

基于 TypeScript 和注解的轻量级IoC容器,供应了依靠注入、面向切面编程及非常处置惩罚等功用。Rockerjs Core可在恣意工程中引入,是一个框架无关的IoC容器。

@rockerjs/core模块不依靠于任何框架,并与现有框架、库、类等坚持兼容。经由历程DI(Dependency Injection)完成代码解耦和依靠解耦,在构建庞杂运用时保证可扩展性与灵活性;同时供应二维编程的才能,基于注解可在各个连接点(Advice)举行非中心营业的操纵,削减代码冗余;末了,它供应一种基于注解设置的浅易非常处置惩罚机制 — Clamp机制,经由历程特定划定规矩婚配非常处置惩罚递次完成处置惩罚。

一、疾速上手

装置

npm install @rockerjs/core

@rockerjs/core最好实践须要连系TypeScript的装潢器一同运用(也可运用接口),因而须要在项目根目录增加
tsconfig.json 文件,并设置编译选项 “
experimentalDecorators”和“
emitDecoratorMetadata”为 true

示例 1

import { Container, Inject } from '@rockerjs/core';

class User {
  id: string = "testId";
  name: string = "testName";
}

class UserService {
  getUser(_id: string): User {
    return new User();
  }
}

@Inject
class ControlDefault {
  @Inject
  userService: UserService;

  test() {
    let user: User = this.userService.getUser("test");
    console.log(user);
  }
}

@Inject('controllor-with-args', new Date())
class ControlDefaultWithArgs {
  name: string;
  time: Date;

  constructor(name: string, time: Date) {
    this.name = name;
    this.time = time;
  }

  @Inject
  userService: UserService;

  test() {
    let user: User = this.userService.getUser("test");
    console.log(user, this.name, this.time);
  }
}

@Inject('controllor1', 'util', new Date())
class Control {
  name: string;
  time: Date;

  constructor(name: string, time: Date) {
    this.name = name;
    this.time = time;
  }

  @Inject
  userService: UserService;

  test() {
    let user: 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完成
let RPC = {
    config: function (cfg: { serviceUrl: string, interfaces: Function[] }) {
        if (cfg.interfaces) {
            cfg.interfaces.forEach((type: FunctionConstructor) => {
                if (type.prototype) {
                    let newObj = {}, proto = type.prototype;
                    let nms = Object.getOwnPropertyNames(proto);
                    if (nms) {
                        nms.forEach((nm) => {
                            if (nm != 'constructor' && typeof(proto[nm]) === 'function') {
                                newObj[nm] = function () {
                                    //{nm:要领名,arguments:参数表},改成挪用长途要求历程
                                    return arguments[0];//test return
                                }
                            }
                        })
                    }
                    Container.provides([type, () => {
                        return newObj;
                    }])
                }
            })
        }
    }
}

//--DEMO--------------------------------------------------------

//1. 接口声明(注重,此处只能运用Concrete class)
class Product {
    getById(id: string): string {
        return null;
    }
}

//2. 运用RPC Framework
RPC.config({
    serviceUrl: null,
    interfaces: [Product]//供应接口形貌,在RPC中构建factory
})

//3. Service class
@Inject
class Service {
    @Inject
    product: Product;

    test() {
        let id: string = 'tid';
        let rst = this.product.getById(id);
        console.log(rst);
    }
}

//4.测试
Container.getObject<Service>('service').test();

二、依靠注入与容器

依靠注入 @Inject

供应了注解 @Inject 来完成依靠的注入,当我们有以下 GetDubboData 类时

class GetDubboData {
    p0: number;
    constructor(p0: number, p1: string) {
        this.p0 = p0;
    }
}

我们能够经由历程以下体式格局实例化这个类,同时传入指定的参数

  1. 直接通报组织函数的参数

    class SomeControl {
        @Inject(1, 'aaa')
        private dubbo: GetDubboData
    }
  1. 给出组织函数的工场函数

    class SomeControl {
        @Inject(function () {
            return [1, 'aaa']
        })
        private dubbo: GetDubboData
    }
  1. 无组织函数或参数为空

    class SomeControl {
        @Inject
        private dubbo: GetDubboData
    }

操纵类实例化容器

默许的实例化要领能够满足开发者的大部分需求,Rockerjs Core 供应了 provides 要领自定义实例化工场,同时供应了猎取类和类实例化函数映射表的要领。

注册、修正类的实例化要领

  • 直接传入类或工场函数

    // 情势一:形如 Container.provides(class extends UserService{})
    Container.provides(
      class extends UserService {
        getUser(id: string): User {
          console.log(1);
          return new User();
        }
      }
    );
  • 传入类及类的工场函数

    // 情势二:形如 Container.provides([UserService,FactoryFunction])
    Container.provides([
      UserService,
      () => {
        return new class extends UserService {
          getUser(id: string): User {
            console.log(2);
            return new User();
          }
        }();
      }
    ]);

猎取实例化要领注册表

getGeneralHashmap()

返回一个组织函数-工场要领映射表, 构造以下

const globalGeneralProviders: Map<FunctionConstructor, Function> = new Map<
  FunctionConstructor,
  Function
>();

手动实例化要领

Container.injectClazzManually 要领供应了直接实例化注册表中的类的功用,参数为组织函数以及想要传入的参数

class SomeControl {
  transGet: GetTransData = Container.injectClazzManually(GetTransData, 1, 2);

  async getProduct(_productId?: number) {
    let json: any = await this.transGet.getDetail(_productId);
    console.log(json);
  }
}

完全例子

假定我们有一个猎取异步数据的抽象类

abstract class GetTransData {
  p0: number
  constructor(p0: number, p1: string) {
      console.log(p0 + p1)
      this.p0 = p0
  }

  abstract async getDetail(_proId: number): Promise<string>;
}

能够经由历程 Container 的 provides API 来指定对应范例的工场函数

Container.provides([GetTransData, (_p0, _p1) => {
  return new class extends GetTransData {
      constructor(p0: number, p1: string) {
          super(p0, p1);
      }

      async getDetail(_id: number): Promise<string> {
          await  ((ms) => new Promise(res => setTimeout(res, ms)))(100)
          return `Hello ${this.p0}`
      }
  }(_p0, _p1);
}]);

终究经由历程 @Inject 要领注入在测试类内里实例化这个对象

@Inject
class SomeControl {
  @Inject(666, 2)
  transGet: GetTransData;

  async getProduct(_productId?: number) {
      let json: any = await this.transGet.getDetail(_productId);
      console.log(json);
  }
}

Container.getObject<SomeControl>('someControl').getProduct();

获得输出效果

668
Hello 666

三、面向切面编程 AOP

面向切面编程(AOP是Aspect Oriented Program的首字母缩写)是指在运行时,动态地将代码切入到类的指定要领、指定位置上的编程头脑。Rockerjs Core 供应了 AOP 编程才能

简朴的例子

假如我们想在下面的 foo 要领实行前后办理

class Test {
  foo() {
    console.log('foo')
  }
}

new Test().foo()

我们能够声明一个日记类,经由历程@Aspect注解声明其为一个切面类,经由历程@Pointcut注解设置想要婚配的类、要领以及须要实行的钩子, 末了经由历程 @Before@After等注解标识被润饰要领在处于对应生命周期时须要实行的要领

import { Aspect, Pointcut, Before, After } from "@rockerjs/core";

@Aspect
class Logger {
  // 必须在静态要领上注册切点
  @Pointcut({
    clazz: Test, // 定位被润饰的类
    rules: ".*foo.*", // 经由历程正则婚配到对应的要领,不填则婚配一切函数
    advices: ["before:printStart", "after"] // 过滤将要实行的钩子 (可仔细到函数名)
  })
  static main() {}

  // 在实行被办理要领前实行的要领
  @Before
  printStart() {
    console.log("log:start:", new Date());
  }

  // 能够指定多个要领
  @Before
  printStart2() {
    console.log("log:start:", new Date());
  }

  // 在实行被办理要领后实行的要领
  @After
  printEnd() {
    console.log("log:end:", new Date());
  }
}

必须在切面类的静态要领上注册切点

Advices(可理解为生命周期,下文用生命周期代指advice)列表

Rockerjs Core 供应了Before, After,After_Throwing, After_Returning,Around等生命周期

  • Before:在被办理函数实行前实行
  • After:在被办理函数实行后实行
  • After_Throwing:在被办理函数抛出非常时实行
  • After_Returning:在被办理函数返回效果后实行
  • Around:在被办理函数实行前后实行,类似于 koa 中间件

@After_Returning

  1. 在 after 后实行
  2. 假如原生函数没有 return 任何东西则不实行
  3. 能够修正返回效果
@After_Returning
printReturn(ctx, result) {
     // ctx 为函数实行上下文
  // result 为函数实行的效果
  result += 666
  return result
}

@After_Throwing

@After_Throwing
printthrow(ctx, ex) {
    // ctx 为函数实行上下文
    // ex 为毛病信息
    console.log('Loggy catch: '+ ex.message);
    console.log(ctx)
}

@Around

@Around
currentTime2(ctx, next) {
    // ctx 为函数实行上下文
    // next 为婚配到的函数
    console.log('before',Date.now());
    let ret = next();
    console.log('after',Date.now(),ret);
    return ret;
}

切面组合

我们能够为某个类同时注册多个切面类,再经由历程 composeAspects 要领将它们组合起来,默许根据声明的递次来包裹被办理的函数,末了声明的类会包裹在最表面一层

@Aspect()
class Logger {
    // ...
}

@Aspect()
class Logger2 {
  @Pointcut({
    clazz: Test,
    advices: ["before", "after", "around", 'after_returning']
  })
  static main() {}

  @Before
  printStart() {
    console.log("2:start");
  }

  @After
  printafter() {
    console.log("2:after");
  }

  @After_Returning
  printReturn(ctx, result) {
      console.log('2:after_returning', result)
      return result + 2
  }

  @Around
  printAround2(ctx, next) {
    console.log("2:around:before");
    let ret = next();
    console.log("2:around:after", ret);
    return ret;
  }
}

@Aspect()
class Logger3 {
  // ...
}

composeAspects({
    clazz: Test,
    // rules: '.*foo.*',
    aspects: [Logger, Logger2, Logger3]
});

实行效果以下:

3:start
2:start
1:start
3:around:before
2:around:before
1:around:before
foo
1:around:after bar
2:around:after bar
3:around:after bar
1:after
2:after
3:after
1:after_returning bar
2:after_returning bar
3:after_returning bar

假如想自定义切面之间实行的递次,能够在切面注解上传入切面的序次(数值小的在洋葱模子的外层):

@Aspect({
  order: 2
})
class Logger { }

@Aspect({
  order: 1
})
class Logger2 { }

@Aspect({
  order: 3
})
class Logger3 { }

composeAspects({
    clazz: Test,
    aspects: [Logger, Logger2, Logger3]
});

实行递次以下:

2:start
1:start
3:start
2:around:before
1:around:before
3:around:before
foo
3:around:after bar
1:around:after bar
2:around:after bar
3:end
1:end
2:end

四、非常处置惩罚 Exception

除了经由历程 Rockerjs Core AOP 中的 @After_Throwing 注解来完成毛病捕捉,我们还供应了更轻便的完成毛病捕捉的要领,以下例,我们先声明一个毛病捕捉夹,然后在被包裹的函数上运用这个毛病捕捉夹,当函数实行历程中有非常发作时,我们能在捕捉夹的 catch 要领中拿到毛病信息以及函数实行的上下文。

import { Container, Inject, Catch, Clamp, ExceptionClamp } from "@rockerjs/core";

// 1. 声明一个捕捉器,完成 catch 要领
@Clamp
class Clamper extends ExceptionClamp {
  catch(ex, ctx) {
    console.log("hahaha: ****", ex, ctx);
  }
}

@Inject
class Test {
  // 2. 运用捕捉器
  @Catch("Clamper")
  test() {
    throw new Error("12322");
  }
}

Container.getObject<Test>('test').test();

@After_Throwing 同时运用时,@Catch 会先捕捉到毛病,再次将毛病抛出, @After_Throwing 才捕捉到毛病

@Clamp
class Clamper extends ExceptionClamp {
  catch(ex, ctx) {
    console.log("hahaha: ****", ex, ctx);
    throw ex // 将毛病二次抛出
  }
}

@Inject
class Test {
  @Catch("Clamper")
  test() {
    throw new Error("12322");
  }
}


@Aspect
class ExceptionClamp2 {
  @Pointcut({
    clazz: Test,
    advices: ['after_throwing']
  })
  static main() {}

  @After_Throwing
  printThrow(ctx, ex) {
      console.log('Loggy catch: '+ ex.message);
      console.log(ctx)
  }
}

Container.getObject<Test>('test').test();

Contribute

请参考 Contribute Guide 后提交 Pull Request。

    原文作者:RoyalRover
    原文地址: https://segmentfault.com/a/1190000018596681
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞