装饰器在TypeScript中的应用

1,328 阅读6分钟

TypeScript对装饰器的语法支持的很好,在实际的项目中装饰器的用途也非常的多,比如:react-redux、sequelize-typescript等等,今天就来对装饰器的使用做一个总结。

TypeScript中装饰器的语法

装饰器首先是一个自定义的函数,具体的逻辑需要自己在函数中编写。TypeScript中的装饰器可以修饰类、属性、方法、访问器(get,set)、方法参数。修饰的对象不同,装饰器函数的参数也不同。下面来回顾一下装饰器如何修饰类和方法。

装饰器修饰类

// target是被修饰类的本身,也就是构造函数。这个地方的target是Person
function testable(target: any) {
  target.isTest = true;
}
@testable
class Person {
}

上面的代码给Person类添加了装饰器,目的就是给Person添加了一个静态的属性isTest,语法也很简单,就是在定义类的上一行用@语法给Person添加了装饰器,@后面跟着方法名。

装饰器修饰方法

function testMethod(target:any,name:string,descriptor: PropertyDescriptor){
  const oldFn = descriptor.value
  descriptor.value = function(){
    console.log('method called')
    oldFn()
  }
}
class Person{
  @testMethod
  getName(){
    return 'mickey'
  }
}
// 会打印出'method called'
new Person().getName()

需要注意的是,修饰类的时候target是指构造函数本身,而在修饰方法时,target指的是被修饰方法所在类的原型,这个地方target就是Person.prototype。

Object.defineProperty(obj,key,descriptor)的第三个参数descriptor大家都知道什么意思,testMethod的descriptor参数的作用也一样。

这里改写了原始的方法getName,在调用时先用console.log打印,然后在调用原始方法。这种场景非常像设计模式中的装饰者模式,保证原有功能的基础上,对原有功能上面添加新功能。

使用装饰器的时候,需要注意一下几点:

  • 和方法在运行时执行不同,装饰器函数是在编译时调用的。
  • 一个方法上如果有多个装饰器,是从下往上执行
  • 如果同时定义了类装饰器和方法装饰器,方法装饰器先被调用,然后才是类装饰器

装饰器的一个小应用

我们经常的写代码的时候会用try catch来捕获异常,如果每个方法内部都try catch的话太麻烦了,这种情况下可以用装饰器类优化一些代码。

看看下面的代码,如果没用装饰器的话,写起来很麻烦

class Person {
  foo() {
    const obj: any = {};
    try {
      obj.foo();
    } catch (e) {
      console.log("foo方法没定义");
    }
  }
  bar() {
    const obj: any = {};
    try {
      obj.bar();
    } catch (e) {
      console.log("bar方法没定义");
    }
  }
}
const p = new Person();
p.foo();
p.bar();

如果用了装饰器,代码可以简化很多,不用每个方法内部都try catch了

function catchError(msg: string) {
  return function(target: any, name: string, descriptor: PropertyDescriptor) {
    // 原来的方法
    const originFn = descriptor.value;
    // 重新定义方法
    descriptor.value = function() {
      try {
        originFn();
      } catch (e) {
        console.log(msg);
      }
    };
  };
}

class Person {
  @catchError("foo方法没定义")
  foo() {
    const obj: any = {};
    obj.foo();
  }
  @catchError("bar方法没定义")
  bar() {
    const obj: any = {};
    obj.bar();
  }
}
const p = new Person();
p.foo();
p.bar();

自定义的方法catchError返回了一个函数,为的就是在调用catchError可以传递msg这个参数,根据不同的需要提示不同的文本。

使用了装饰器修饰方法后,以后任何方法只要需要try catch,那么在方法上加上@catchError即可,很方便

node中Controller使用装饰器提高代码可读性

在做node的项目时,我们一般都是用koa框架结合koa-router来做路由管理,这就意味着我们必须用代码定义很多路由,以及路由对应的处理函数。有了装饰器后,定义路由以及处理函数的工作,我们可以让装饰器来做,这样就可以精简我们的代码,让代码的可读性更强。先来看看使用装饰器后的代码结构:

import "reflect-metadata";
import { Context } from "koa";
import { controller, get } from "../decorators";

@controller
class HomeController {
  @get("/")
  index(ctx: Context) {
    ctx.body = {
      data: []
    };
  }
}

export default new HomeController()

controller和get是自定义的2个装饰器

先看看get装饰器的实现逻辑:

import "reflect-metadata";
import { Method } from "../types";

function getDecoratorFn(method: Method) {
  return function get(url: string) {
    return function(target: any, name: string, descriptor: PropertyDescriptor) {
      Reflect.defineMetadata("url", url, target, name);
      Reflect.defineMetadata("method", method, target, name);
    };
  };
}

export const get = getDecoratorFn("get");

这里用了一个reflect-metadata的库文件,用来在类或者方法上定义元数据。

用Reflect.defineMetadata把 url访问路径url 和 请求方法method 的值作为元数据存储在原型的方法上,以便后续用Reflect.getMetadata获取到

在来看controller的实现逻辑:

import "reflect-metadata";

export function controller(target: any) {
  for (let name in target.prototype) {
    const url = Reflect.getMetadata("url", target.prototype, name);
    const method: Method = Reflect.getMetadata(
      "method",
      target.prototype,
      name
    );
    const handle = target.prototype[name];
    if (!url) return;
    router[method](url, ...handle);
  }
}

controller函数首先遍历定义在类的原型上的方法,看看在方法上有没有定义装饰器,如果定义了装饰器就根据定义在方法上的元数据的信息生成一条路由,并且把改路由和处理函数关联起来,这样我们就用controller装饰器封装了路由和处理函数之间的对应关系。

在koa中我们有时候会用到中间件,上面的代码并不支持中间件,所以修改controller的代码:

export function controller(target: any) {
  for (let name in target.prototype) {
    const url = Reflect.getMetadata("url", target.prototype, name);
    const method: Method = Reflect.getMetadata(
      "method",
      target.prototype,
      name
    );
   const middlewares: Middleware[] =
      Reflect.getMetadata("middleware", target.prototype, name) || [];
    const handle = target.prototype[name];
    const handles = [handle];
    if (!url) return;
    if (middlewares.length) {
      handles.unshift(...middlewares);
    }
    router[method](url, ...handles);
  }
}

再添加一个中间件的装饰器:

export default function middleware(midFn: Middleware) {
  return function(target: any, name: string, descriptor: PropertyDescriptor) {
    const middlewares: Middleware[] =
      Reflect.getMetadata("middleware", target, name) || [];
    middlewares.push(midFn);
    Reflect.defineMetadata("middleware", middlewares, target, name);
  };
}

middleware函数接收一个中间件作为参数。因为我们可以使用多个中间件,所以代码中用了数组来保存每个中间件。

加入了中间件后,我们的代码就可以这么来使用中间件:

import "reflect-metadata";
import { Context } from "koa";
import { controller, get, middleware } from "../decorators";
import log from "../middlewares/log";
import log2 from "../middlewares/log2";

@controller
class HomeController {
  @get("/")
  @middleware(log)
  @middleware(log2)
  index(ctx: Context) {
    ctx.body = {
      data: []
    };
  }
}

export default new HomeController();

log和log2是自定义的2个中间件

装饰器在前端的应用

在做React的项目时,我们经常会使用高阶组件(HOC)来实现组件的复用。如果配合装饰器,那么代码看起来会更加简洁。

// 接收一个组件,返回一个新组件
function wrap(WrappedComponent) {
  const style = {
    border: "1px solid red"
  };
  return function() {
    return (
      <div style={style}>
        <WrappedComponent />
      </div>
    );
  };
}

@wrap
class HelloWord extends React.Component {
  render() {
    return <div>helloword</div>;
  }
}

export default function App() {
  return (
    <div className="App">
      <HelloWord />
    </div>
  );
}

我们首先定义了一个HelleWorld组件,然后定义了一个装饰器用来给组件加一个红色边框。