面向複雜應用,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。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章