Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

基于 Typescript 和 Decorator 实现依赖注入 #34

Open
shaozj opened this issue Oct 8, 2019 · 0 comments
Open

基于 Typescript 和 Decorator 实现依赖注入 #34

shaozj opened this issue Oct 8, 2019 · 0 comments
Assignees

Comments

@shaozj
Copy link
Owner

shaozj commented Oct 8, 2019

基于 Typescript 和 Decorator 实现依赖注入

依赖注入在软件工程中有大量的实践应用,常见的服务端框架如 Spring 等都是基于依赖注入的方式来管理对象之间的依赖关系。对于 java 开发同学来说,依赖注入是再熟悉不过的技术了。nestjs 是基于依赖注入的 nodejs web 框架。类似 java 的 spring。大量使用 decorator,在写法上和 spring 十分类似。本文将探讨如何基于 Typescript 和 Decorator 实现依赖注入。

什么是依赖注入

依赖注入是将一个对象所依赖的其他对象直接提供给这个对象,而不是在当前对象中直接构建这些依赖的对象。

为什么要使用依赖注入

  • 便于单元测试
  • 解耦,统一管理被依赖对象的实例化,不用在类的内部创建被依赖对象

如何实现依赖注入

Typescript 中的装饰器 Decorator

装饰器是一种特殊类型的声明,它能够被附加到类声明,方法,访问符,属性或参数上。 装饰器使用@expression这种形式,expression求值后必须为一个函数,它会在运行时被调用,被装饰的声明信息做为参数传入。例如,有一个@Sealed装饰器,我们会这样定义和使用sealed函数:

function sealed(target) {
    // do something with "target" ...
}

@sealed
class MyClass {}

装饰器工厂

装饰器工厂就是一个简单的函数,它返回一个装饰器。
我们可以通过下面的方式来写一个装饰器工厂函数:

function color(value: string) { // 这是一个装饰器工厂
    return function (target) { //  这是装饰器
        // do something with "target" and "value"...
    }
}

类装饰器

类装饰器在类声明之前被声明(紧靠着类声明)。 类装饰器应用于类构造函数,可以用来监视,修改或替换类定义。类装饰器表达式会在运行时当作函数被调用,类的构造函数作为其唯一的参数。如果类装饰器返回一个值,它会使用提供的构造函数来替换类的声明。

@sealed
class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        return "Hello, " + this.greeting;
    }
}

function sealed(constructor: Function) {
    Object.seal(constructor);
    Object.seal(constructor.prototype);
}

方法、属性、访问器的装饰器

方法、属性、访问器装饰器表达式会在运行时当作函数被调用,传入下列3个参数:

  1. 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  2. 成员的名字。
  3. 成员的属性描述符。

对于属性装饰器,第3个参数为 undefined。

Typescript 中的 Reflect Metadata

Reflect Metadata 是 ES7 的一个提案,它主要用来在声明的时候添加和读取元数据。TypeScript 在 1.5+ 的版本已经支持它,要使用 reflect metadata,你需要:

  • npm i reflect-metadata --save
  • tsconfig.json 里配置 emitDecoratorMetadata 选项为 true
{
  "compilerOptions": {
    "target": "ES2015",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

Reflect Metadata 的 API 可以用于类或者类的属性上,其声明如下:

function metadata(
  metadataKey: any,
  metadataValue: any
): {
  (target: Function): void;
  (target: Object, propertyKey: string | symbol): void;
};

Reflect.metadata 可以当作 Decorator 使用,当修饰类时,在类上添加元数据,当修饰类属性时,在类原型的属性上添加元数据,如:

@Reflect.metadata('inClass', 'A')
class Test {
  @Reflect.metadata('inMethod', 'B')
  public hello(): string {
    return 'hello world';
  }
}

console.log(Reflect.getMetadata('inClass', Test)); // 'A'
console.log(Reflect.getMetadata('inMethod', new Test(), 'hello')); // 'B'

Reflect metadata 有强大的功能,包括获取类类型信息和自定义元数据信息以及获取自定义元数据信息。

获取类型信息

function Prop(): PropertyDecorator {
  return (target, key: string) => {
    const type = Reflect.getMetadata('design:type', target, key);
    console.log(`${key} type: ${type.name}`); // Aprop type: string
  };
}

class SomeClass {
  @Prop()
  public Aprop!: string;
}

在装饰器函数中可以通过下列三种内置的 metadataKey 获取类型信息。

  • design:type: 属性类型
  • design:paramtypes: 参数类型
  • design:returntype: 返回值类型

自定义 metadataKey

Reflect Metadata 除能获取内置类型信息外,还可用于自定义 metadataKey,并在合适的时机获取它的值,示例如下:

function classDecorator(): ClassDecorator {
  return target => {
    // 在类上定义元数据,key 为 `classMetaData`,value 为 `a`
    Reflect.defineMetadata('classMetaData', 'a', target);
  };
}

function methodDecorator(): MethodDecorator {
  return (target, key, descriptor) => {
    // 在类的原型属性 'someMethod' 上定义元数据,key 为 `methodMetaData`,value 为 `b`
    Reflect.defineMetadata('methodMetaData', 'b', target, key);
  };
}

@classDecorator()
class SomeClass {
  @methodDecorator()
  someMethod() {}
}

Reflect.getMetadata('classMetaData', SomeClass); // 'a'
Reflect.getMetadata('methodMetaData', new SomeClass(), 'someMethod'); // 'b'

基于 typescript 实现依赖注入以及 Controller Get 装饰器

在前面我们介绍了 typescript 中的 decorator 和 reflect-metadata。这些都是为实现依赖注入做的基础准备,下面将介绍如何基于以上技术实现依赖注入。以及基于 decorator 实现 node web 框架中的 Controller Get 等装饰器。

通过构造函数注入

import 'reflect-metadata';

type Constructor<T=any> = new (...args: any[]) => T;

const Injectable = (): ClassDecorator => target => { };

class OtherService {
  a = 1;
}

// 通过构造函数注入
@Injectable()
class TestService {
  constructor(public readonly otherService: OtherService) { }

  testMethod() {
    console.log(this.otherService);
  }
}

const Factory = <T>(target: Constructor<T>): T => {
  // 获取所有注入的服务
  const providers = Reflect.getMetadata('design:paramtypes', target);
  const args = providers.map((provider: Constructor) => new provider());
  return new target(...args);
};

Factory(TestService).testMethod(); // OtherService {a: 1}

上面是一个简单的通过构造函数实现依赖注入的例子。这里可能会有一个疑问,装饰器 Injectable() 似乎什么都没做。但是,把 Injectable() 装饰器去掉后,我们就无法实现依赖注入了。原因是什么呢?我们可以去编译后的代码看下:

// 通过构造函数注入
var TestService = /** @class */ (function () {
    function TestService(otherService) {
        this.otherService = otherService;
    }
    TestService.prototype.testMethod = function () {
        console.log(this.otherService);
    };
    TestService = __decorate([
        Injectable(),
        __metadata("design:paramtypes", [OtherService])
    ], TestService);
    return TestService;
}());

只有添加了 Injectable() 装饰器后才有下面这段代码,这段代码将构造函数的参数类型信息存储到了 metadata 中,使得之后在实例化时能够获取到构造函数参数的类型。

TestService = __decorate([
    Injectable(),
    __metadata("design:paramtypes", [OtherService])
], TestService);

通过类成员方法参数注入

// 通过类成员方法参数注入
const MethodInjectable = (): MethodDecorator => (
  target: Object,
  propertyKey: string | symbol,
  descriptor: TypedPropertyDescriptor<any>
) => {
  let method = descriptor.value;
  descriptor.value = function (...args) {
    const providers = Reflect.getMetadata('design:paramtypes', target, propertyKey);
    const providersInsts = providers.map(P => new P());

    return method.apply(this, [...providersInsts, ...args]);
  }
};

class TestParamInjectService {
  greeting: string;

  constructor(message: string) {
    this.greeting = message;
  }

  @MethodInjectable()
  greet(otherService: OtherService) {
    console.log(otherService);
  }
}

const test = new TestParamInjectService('test');
test.greet();  // OtherService {a: 1}

在类成员方法参数依赖注入中,我们用到了类成员方法装饰器中的描述符 descriptor。descriptor.value 为该成员方法的值,我们要修改类成员方法,修改 descriptor.value 即可。

Controller 与 Get 的实现(基于 Decorator)

如果你在使用 TypeScript 开发 Node 应用,例如基于 nestjs 开发 node web 应用,相信你对 Controller、Get、POST 这些 Decorator,并不陌生:

@Controller('/test')
class SomeClass {
  @Get('/a')
  someGetMethod() {
    return 'hello world';
  }

  @Post('/b')
  somePostMethod() {

  }
}
const METHOD_METADATA = 'method';
const PATH_METADATA = 'path';

function Controller(path: string): ClassDecorator {
  return target => {
    Reflect.defineMetadata(PATH_METADATA, path, target);
  }
}

const createMappingDecorator = (method: string) => (path: string): MethodDecorator => {
  return (target, key, descriptor) => {
    Reflect.defineMetadata(PATH_METADATA, path, descriptor.value);
    Reflect.defineMetadata(METHOD_METADATA, method, descriptor.value);
  }
}

const Get = createMappingDecorator('GET');
const Post = createMappingDecorator('POST');

@Controller('/test')
class SomeClass {
  @Get('/a')
  someGetMethod() {
    return 'hello world';
  }

  @Post('/b')
  somePostMethod() {

  }
}

function isFunction(functionToCheck) {
  return functionToCheck && {}.toString.call(functionToCheck) === '[object Function]';
}

function mapRoute(instance: Object) {
  const prototype = Object.getPrototypeOf(instance);

  // 筛选出类的 methodName
  const methodsNames = Object.getOwnPropertyNames(prototype)
    .filter(item => item !== 'constructor' && isFunction(prototype[item]));
  return methodsNames.map(methodName => {
    const fn = prototype[methodName];

    // 取出定义的 metadata
    const route = Reflect.getMetadata(PATH_METADATA, fn);
    const method = Reflect.getMetadata(METHOD_METADATA, fn);
    return {
      route,
      method,
      fn,
      methodName
    };
  });
}

// 得到一些有用的信息
Reflect.getMetadata(PATH_METADATA, SomeClass); // '/test'

const routes = mapRoute(new SomeClass());
console.log(JSON.stringify(routes));

输出结果如下:

[
    {
        "route": "/a",
        "method": "GET",
        "methodName": "someGetMethod"
    },
    {
        "route": "/b",
        "method": "POST",
        "methodName": "somePostMethod"
    }
]

通过 Reflect.getMetadata 将类的路由取出。通过 mapRoute 将存储在成员函数上的路由和方法信息提取出来,映射成 route,提取出有用的信息。最后,只需把 route 相关信息绑在 express 或者 koa 上就 ok 了。

更多

  • 自动扫描
  • 循环依赖问题的解决

在 java spring 中,完整的依赖注入还需要自动扫描功能,在 spring 应用启动的时候,会自动扫描@Injectable 并自动完成注入工作,而不需要Factory(TestService)这么写了。
在自动扫描的过程中,我们需要注意到不能出现循环依赖,或者在扫描过程中处理掉循环依赖问题。要解决循环依赖问题,可以将服务的依赖关系构造成一个有向图,具体实现是先将当前的服务推入栈中,再逐层递归遍历服务的依赖插入图中(深度优先遍历)。有向图中存在环则存在循环依赖。有向图构造出来之后拿出图中所有出度构成的依赖数组,因为依赖关系是逐层往上的,即将 A 服务所依赖的其他服务依次实例化,最后再实例化 A ,一直到全部实例化完成为止。

nestjs 源码研究

在 nestjs 中,大量使用了 decorator 和 reflect metadata,如果要深入研究,那么可以深入到其源码中进行分析研究。

参考文献

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant