TypeScript 中装饰器 decorator 的知识
本文主要介绍关于 TS 中装饰器 Decorator 的相关内容以及使用场景;介绍如何实现一个简单DI依赖注入例子;
为什么需要装饰器?
新技术的出现都是在解决问题,TypeScript 中的装饰器实际是实现了 ECMAScript 关于装饰器的提案。目的是解决以下问题:
- 元数据注入(reflect-metadata): 可以为类、方法或属性上添加元数据,这些元数据在运行时被动态访问和使用【想想 DI,依赖注入的场景,大部分都是靠装饰器来进行依赖关系的传递】。
- 功能扩展:可以在不修改原始类的定义情况下,对功能进行扩展和修改。比如:添加埋点、处理日志记录、权限校验等。这个功能和装饰器设计模式功能一致。设计模式的使用能让我们的代码设计更加优雅(更容易理解,健壮性也更好)。
- 代码重用:一个装饰器函数可以在多个类、方法、属性上进行重复使用。装饰器设计模式很难这点(要做到就很难,抽象层次很高,把装饰器类变成公交车,谁都能用才行)。
装饰器的使用以及分类
我们把装饰器函数叫做装饰器(实际上就是一个函数),把应用@decoratorFunc 的方式叫做装饰器应用。来个简单的demo:
// 使用 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";
};
}
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 演示
@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
// 属性装饰器函数
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 上,库依赖好处理不用自己到处安装。
npm install inversify reflect-metadata
// interface.ts
export interface ILogger {
log(msg: string): void;
}
export interface IService {
doing(): void;
}
export const LoggerID = Symbol("Logger");
export const ServiceId = Symbol("Service");
// 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!!");
}
}
// 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);
// 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>
);
}
下篇我们聊聊 metadata 元数据。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。