TypeScript 中装饰器 decorator 的知识

# TypeScript 中装饰器 decorator 的知识

本文主要介绍关于 TS 中装饰器 Decorator 的相关内容以及使用场景;介绍如何实现一个简单DI依赖注入例子; 

## 为什么需要装饰器?

新技术的出现都是在解决问题,TypeScript 中的装饰器实际是实现了 ECMAScript 关于装饰器的提案。目的是解决以下问题:

* **元数据注入(reflect-metadata)**: 可以为类、方法或属性上添加元数据,这些元数据在运行时被动态访问和使用【想想 DI,依赖注入的场景,大部分都是靠装饰器来进行依赖关系的传递】。

* **功能扩展**:可以在不修改原始类的定义情况下,对功能进行扩展和修改。比如:添加埋点、处理日志记录、权限校验等。这个功能和装饰器设计模式功能一致。设计模式的使用能让我们的代码设计更加优雅(更容易理解,健壮性也更好)。

* **代码重用**:一个装饰器函数可以在多个类、方法、属性上进行重复使用。装饰器设计模式很难这点(要做到就很难,抽象层次很高,把装饰器类变成公交车,谁都能用才行)。

## 装饰器的使用以及分类

我们把装饰器函数叫做装饰器(实际上就是一个函数),把应用@decoratorFunc 的方式叫做装饰器应用。来个简单的demo:

``` typescript

// 使用 webDecorator 修饰器来修饰 User 类

@webDecorator

export class User {

  private name: string;

  constructor(name: string) {

    this.name = name;

  }

}

// 定义一个类修饰器, 只需要定义一个参数

function webDecorator<T extends { new (...args: any[]): {} }>(

  TargetConstructor:T

) {

  return class extends TargetConstructor {

    private registerOrigin = "WEB-SITE";

  };

}

// 定义一个类修饰器

function appDecorator<T extends { new (...args: any[]): {} }>(

  TargetConstructor:T

) {

  return class extends TargetConstructor {

    private registerOrigin = "APP";

  };

}

```

``` typescript

import { User } from './user.ts';

function testUserRegister() {

  const user = new User('Tony');

  console.log(user);

  // {"name":"Tony","registerOrigin":"WEB-SITE"}


}

testUserRegister();

```

输出的结果:

装饰器的类型按照应用的目标类型进行分类,不同类别的装饰器函数签名不同。分类如下:

* **类装饰器(Class Decorators )**,应用与类声明之前的装饰器,可以用来修饰类的行为(比如附加一些函数方法)、添加元数据或静态成员。

  * 参数:类装饰器接收一个参数,也就是被装饰类的构造函数(看上边的一个列子demo)。

  * 示例:`function classDecorator(target: { new(): {} }) { ... }`

  * demo 演示(参见上一个例子)

* **方法装饰器(Method Decorators),用于类方法声明之前的装饰器**,可以用来修改方法的行为、可以进行参数校验甚至补充逻辑,是执行一些与业务逻辑无关的副作用的最佳选择(比如:发送埋点、日志记录、权限检查等等)。

  * 参数:接受三个参数,分别是被装饰的类的原型对象、方法名称和属性描述符。

  * 示例:`function methodDecorator(target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) { ... }`

  * demo 演示

``` typescript

@webDecorator

export class User {

  private name: string;

  constructor(name: string) {

    this.name = name;

  }

  @methodDecorator

  greet(str: string) {

    return this.name + ":" + str;

  }

  say() {}

}

// 定义一个 methodDecorator

function methodDecorator(

  target: object,

  propertyKey: string | symbol,

  descriptor: PropertyDescriptor

) {

  const value = descriptor.value;

  descriptor.value = function (...args: any[]) {

    const result = value.apply(this, args);

    //...

    return result + "##";

  };

  return descriptor;

}

// 定义一个类修饰器

function webDecorator<T extends { new (...args: any[]): {} }>(

  TargetConstructor: T

) {

  return class extends TargetConstructor {

    private registerOrigin = "WEB-SITE";

  };

}

```

* **属性修饰器(Property Decorators),应用于类的属性声明之前的装饰器**,一般用来修改属性的行为或者添加元数据。

  * 参数:接收两个参数,分别是被修饰的类的原型对象和属性名称。

  * 示例:`function propertyDecorator(target: Object, propertyKey: string | symbol){...}`

  * demo

``` typescript

// 属性装饰器函数

function logProperty(target: any, propertyKey: string) {

  let value = target[propertyKey];

  // 属性 getter

  const getter = function () {

    console.log(`Getting value of property ${propertyKey}: ${value}`);

    return value;

  };

  // 属性 setter

  const setter = function (newValue: any) {

    console.log(`Setting value of property ${propertyKey} to: ${newValue}`);

    value = newValue;

  };

  // 重新定义属性

  Object.defineProperty(target, propertyKey, {

    get: getter,

    set: setter,

    enumerable: true,

    configurable: true,

  });

}

// 示例类

class MyClass {

  @logProperty

  myProperty: string = 'Initial value';

}

// 使用示例

const instance = new MyClass();

console.log(instance.myProperty);  // 获取属性值

instance.myProperty = 'New value'; // 设置属性值

console.log(instance.myProperty);  // 再次获取属性值

```

* **参数装饰器(Parameter Decorators 应用于类参数或函数参数申明之前的装饰器**。这个就不说了,和属性修饰器类似。

## DI 依赖注入

借助TypeScript提供的 装饰器能力以及 reflect-metadata 处理元数据的能力,InversfyJS 提供了DI(dependency injection)实现。DI 的使用能大幅提升代码的松耦合表现,依赖之间基于接口和抽象而不是具体实现。

tips:代码调试建议在 codesandbox 上,库依赖好处理不用自己到处安装。

``` shell

npm install inversify reflect-metadata

```

``` typescript

// interface.ts

export interface ILogger {

  log(msg: string): void;

}

export interface IService {

  doing(): void;

}

export const LoggerID = Symbol("Logger");

export const ServiceId = Symbol("Service");

```

``` typescript

// service.ts

import { injectable, inject } from "inversify";

import { ILogger, IService, LoggerID } from "./interface";

import "reflect-metadata";

// 定义一个处理日志的类实现 ILogger 接口

@injectable()

export class Logger implements ILogger {

  log(msg: string) {

    console.log("logger: ", msg);

  }

}

// 定义一个消费 Logger 的 Example 类,这里 logger 并不用我们自己初始化 new , 而是使用 inject 装饰器来获取。

@injectable()

export class Example implements IService {

  @inject(LoggerID)

  private logger: ILogger;

  // 代码依赖于接口,而不依赖具体实现了。爽歪歪

  doing() {

    this.logger.log("your DI is runing!!");

  }

}

```

``` typescript

// container.ts

import { Container } from "inversify";

import { ILogger, IService, LoggerID, ServiceId } from "./interface";

import { Logger, Example } from "./service";

import "reflect-metadata";

// 初始化容器

export const DI = new Container({ autoBindInjectable: true });

// 向容器中绑定映射对象, 容器会默认进行初始化,不用操心。

DI.bind<ILogger>(LoggerID).to(Logger);

DI.bind<IService>(ServiceId).to(Example);

```

``` typescript

// app.tsx

import "./styles.css";

import { IService, ServiceId } from "./interface.ts";

import { DI } from './container.ts';

export default function App() {

  const handleClick = () => {

    const service = DI.get<IService>(ServiceId);

    service.doing();

  };

  return (

    <div className="App">

      <button onClick={handleClick}>DI</button>

    </div>

  );

}

```

![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/fbff5ad4ec624e2fb13034b91f3a14d7~tplv-k3u1fbpfcp-zoom-1.image)

下篇我们聊聊 metadata 元数据。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 222,104评论 6 515
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,816评论 3 399
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 168,697评论 0 360
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,836评论 1 298
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,851评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,441评论 1 310
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,992评论 3 421
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,899评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,457评论 1 318
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,529评论 3 341
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,664评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,346评论 5 350
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 42,025评论 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,511评论 0 24
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,611评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 49,081评论 3 377
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,675评论 2 359

推荐阅读更多精彩内容

  • 为什么 TypeScript Vue2.x 对 TypeScript 的支持是硬伤,而 TypeScript 对于...
    丶梅边阅读 3,098评论 0 2
  • 本文记录了 TypeScript 中的基础变量类型和使用方式,以及在 Vue2 框架中引入的调整。 类型 布尔值l...
    聚散浮生乄阅读 255评论 0 0
  • 访问器装饰器 访问器装饰器声明在一个访问器的声明之前(紧靠着访问器声明)。 访问器装饰器应用于访问器的 属性描述符...
    2o壹9阅读 1,046评论 1 49
  • 概述 TypeScript本质上是向JavaScript语言添加了可选的静态类型和基于类的面向对象编程,同时也支持...
    oWSQo阅读 8,525评论 1 45
  • 一、TS 快速上手 1. 关于TS TypeScript 是 JavaScript 的一个超集,可以编译成纯 Ja...
    O蚂蚁O阅读 7,782评论 0 0