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;
}
}
我們可以通過以下方式實例化這個類,同時傳入指定的參數
-
直接傳遞構造函數的參數
class SomeControl { @Inject(1, 'aaa') private dubbo: GetDubboData }
-
給出構造函數的工廠函數
class SomeControl { @Inject(function () { return [1, 'aaa'] }) private dubbo: GetDubboData }
-
無構造函數或參數爲空
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
- 在 after 後執行
- 如果原生函數沒有 return 任何東西則不執行
- 可以修改返回結果
@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。