学习Nestjs有必要梳理一下TypeScript

3,667 阅读5分钟

最近在学习NestJs 过程中发现,NestJs 大量借鉴了 Angular 和 spring 的,引入了AOP面向切面编程的思想。什么是面向切面编程呢?一个正常的http请求经过 Controller(控制器)、Service(服务)、Repository(数据库访问)的过程中,如果需要加入日志记录、权限控制、异常处理等链路。我们可以避开直接改造Controller 层会带来的不优雅的后果,而是在调用 Controller 之前和之后加入一个执行通用逻辑,就像切了一刀一样。这样的横向扩展点就叫做切面,这种透明的加入一些切面逻辑的编程方式就叫做 AOP。

NestJs实现这种AOP思想的核心大多是通过@Module、 @Controller@Injectable@UsePipes@SetMetadata@UseFilters@UseInterceptors等核心装饰器来实现的。ts的装饰器和java的注解非常相似,都可以通过添加源数据支持,虽然在语法上很相似,但是不同的语言之间使用的方法和概念上有所差异:

  • 使用注解(Annotation)的语言:Java、C#(叫 Attribute)。
  • 使用装饰器(Decorator)的语言:Python、TypeScript。

现在 JavaScript中的装饰器提案已经进入到 stage 3 阶段了,相信不久之后不用ts也可以使用。

装饰器(Decorator)

ts的装饰器是一种特殊的声明(只能声明为函数),可以附加到方法属性参数上,装饰者使用 @函数名 形式来修改类的行为。常见的装饰器有:

  • 类装饰器;
  • 属性装饰器;
  • 方法装饰器;
  • 参数装饰器;

而装饰器的写法又分为两种它们分别是:

  • 普通装饰器(无法传参数);
  • 装饰器工厂(可以传参数);

类装饰器 (普通装饰器)

  • 类装饰器在类声明之前声明(紧靠着类声明),可以用来监视修改或者替换类定义。
  • 类装饰器表达式会在运行时当作函数被调用,类的构造函数作为其唯一的参数
function ClassDecorator(target: any) {
  console.log(target === Services); // true
  target.prototype.name = "kobe";
}

@ClassDecorator
class Services {
  constructor() {}
}

const s: any = new Services();
console.log(s.name); // 'kobe'

通过下面的例子可以看到:装饰器的第一个参数是类的构造函数,可以对类进行修改,覆盖、添加或者删除类里面的属性及方法:

function DecoratorTest(target: any) {
  target.say = function () {
    console.log('hello')
  }
  target.run = function () {
    console.log('world')
  }
}

@DecoratorTest
class Animal {
  static say: Function;
  constructor() {
  }
}

Animal.say() // hello
Animal.run() // world

当类装饰器有返回值时,返回的值会替换原有的类的定义:

function DecoratorStr(target) {  
  return 'hello world';  
}  
  
function DecoratorClass(target) {  
  return target;  
}  
  
@DecoratorStr  
class A { }  
  
@DecoratorClass  
class B { }  
  
console.log(A); // hello world
console.log(B); // ƒ ClassB()

类装饰器(装饰器工厂)

如果想定义一个装饰器工厂也很简单,它也是一个函数。和前面的不同的是,它又返回一个函数,这个被返回的函数接收一个参数,这个参数就是被装饰的类。具体代码如下所示:

// 装饰器工厂可以带参数传入
function ClassDecorator(params: string) {
  return function (target) {
    console.log(target === Services); // true
    target.prototype.name = params; // 携带的参数可以直接赋值
  };
}

@ClassDecorator("kobe")
class Services {
  constructor() {}
}

const s: any = new Services();
console.log(s.name); // kobe

方法装饰器

  • 方法装饰器用来装饰方法
    • 第一个参数: 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
    • 第二个参数: 是方法的名称
    • 第三个参数: 是方法的属性描述符
  • 如果一个类方法装饰器有返回值,则返回值会被用作方法的属性描述符
function MethodDecorator() {
  return function (target: any, propertyKey: any, descriptor: any) {
    console.log(target);
    console.log(propertyKey);
    console.log(descriptor);
  };
}

class Services {
  constructor() {}

  @MethodDecorator()
  getDate() {}
}

// 输出结果:
'Services: {}'
'getDate'
{
  "writable": true,
  "enumerable": fasle,
  "configurable": true
}

如下所示方法装饰器的属性描述符是可以被修改的:

function DecoratorReadonly(target, name, descriptor){
  descriptor.writable = false
  return descriptor;
}

class Person {
  @DecoratorReadonly
  name() { return 'kobe' }
}

console.log(Object.getOwnPropertyDescriptor(Person, 'name'))
// { value:"Person", writable:false, enumerable:false, configurable:true}

属性装饰器

  • 属性装饰器用来装饰属性
  • 属性装饰器表达式会在运行时当做函数被调用,传入两个参数:
    • 第一个参数: 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
    • 第二个参数: 是属性的名称
  • 属性装饰器返回值会被忽略。属性描述符只能用来监视类中是否声明了某个名字的属性
function PropertyDecorator(params) {
  return function (target: any, propertyKey: any) {
    console.log(target); 
    console.log(propertyKey);
    console.log(params);
  };
}
class Services {
  @PropertyDecorator("kobe")
  public name: number;
}
const s: any = new Services();

// 输出结果:
'Services: {}'
'name'
'kobe'

参数装饰器

  • 参数装饰器用来装饰参数
    • 第一个参数: 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
    • 第二个参数: 成员的名字
    • 第三个参数: 参数在函数参数列表中的索引
  • 参数装饰器只能用来监视一个方法的参数是否被传入,返回值会被忽略
function ParameterDecorator(params: any) {
  return function (target: any, propertyKey: any, parameterIndex: number) {
    console.log(params);
    console.log(target);
    console.log(propertyKey);
    console.log(parameterIndex);
  };
}

class Services {
  constructor() {}

  getDate(@ParameterDecorator("kobe") value: any) {
    console.log(value);
  }
}

const s: any = new Services();
s.getDate("james");

// 输出结果:
"kobe"
"Services {}"
"getDate"
0
"james"

装饰器执行顺序

装饰器的执行顺序依赖ts执行的上下文,谁先执行完就先调用谁(方法参数是从后到前,方法也是)。具体代码如下所示:

function test01(params: any) {
  console.log("类装饰器");
}
function test02(params: any) {
  return function (target: any, propertyKey: any) {
    console.log("属性装饰器");
  };
}
function test03(params: any) {
  return function (target: any, propertyKey: any, descriptor: any) {
    console.log("方法装饰器");
  };
}
function test04(params: any) {
  return function (target: any, propertyKey: any, parameterIndex: number) {
    console.log("参数装饰器");
  };
}

function test05(params: any) {
  return function (target: any, propertyKey: any, parameterIndex: number) {
    console.log("参数装饰器1");
  };
}

@test01
class Services {
  constructor() {}
  
  @test03("")
  getDate(@test04("") year: any, @test05("") month: any) {}
  
  @test02("")
  public name: number | undefined;
}

const s: any = new Services();
s.getDate("nba");

// 输出结果
"参数装饰器1"
"参数装饰器"
"方法装饰器"
"属性装饰器"
"类装饰器"

参考资料:

  1. typescript官方网站