zoukankan      html  css  js  c++  java
  • 面向复杂应用,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
      }
      
    2. 给出构造函数的工厂函数

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

      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。

  • 相关阅读:
    Window如何查看cpu核数,更改CPU开启的核数?
    Mysql5.6.47开放远程访问(修改远程访问密码)
    CentOS7.6新增或修改SSH端口号的步骤
    虚拟机下安装Centos设置静态ip,并通过桥接连接
    windows下安装mysql5.6.47版本
    微软官方安装介质Windows10系统安装教程
    【测试编码URI的函数】
    【JavaScript函数】
    【JavaScript运算符与表达式】
    【JavaScript声明变量的规则】
  • 原文地址:https://www.cnblogs.com/accordion/p/10569848.html
Copyright © 2011-2022 走看看