原文链接: https://leanpub.com/essentialtypescript/read#decorators
1. Function Decorator
到 TodoService.ts 中,修改add()方法,在函数开头和结尾处打印log:
add(input): Todo {
console.log(`add(${JSON.stringify(input)})`);
// ...
// 函数功能性代码
// ...
console.log(`add(${JSON.stringify(input)}) => ${JSON.stringify(todo)}`);
return todo;
};
如果我们要让其它的函数也打印log,是不是要去修改每一个函数?使用Decorator,可以把log功能独立出来,不必把log代码嵌入函数体。
首先,假设我们有个Function decorator叫做 log,用它来修饰 add() 方法:
@log
add(input) {
// ...
}
然后,定义这个log decorator,它是一个function:
function log(target: Object, methodName: string, descriptor: TypedPropertyDescriptor<Function>) {
var originalMethod = descriptor.value;
descriptor.value = function(...args) {
console.log(`${methodName}(${JSON.stringify(args)})`);
let returnValue = originalMethod.apply(this, args);
console.log(`${methodName}(${JSON.stringify(args)}) => ${JSON.stringify(returnValue)}`);
return returnValue;
}
}
这个函数的signature是固定的,所谓的 Function decorator必须带有这三个参数,这些参数提供了要修饰的function信息:
1. target 是function所在的对象(即TodoService的实例);
2. methodName 是函数名(即 add);
3. descriptor 包含了要修饰的函数的所有metadata,其中,descriptor.value 是函数体。
这个log decorator可以用来修饰任何函数。
2. Class Decorators
1. 实现一个class decorator
Class Decorator 和 Function Decorator 的用法类似:
//Validators.ts
@validatable
export class ValidatableTodo implements Todo {
id: number;
name: string;
state: TodoState;
}
export function validatable(target: Function) {
target.prototype.validate = function() {
let validators: IValidator[] = this._validators ? [].concat(this._validators) : [];
let errors: IValidationResult[] = [];
for(let validator of validators) {
let result = validator(this);
if(!result.isValid) {
errors.push(result);
}
}
return errors;
};
}
export interface IValidationResult {
isValid: boolean;
message: string;
property?: string;
}
export interface IValidator {
(instance: Object): IValidationResult;
}
说明:
1. 在 TypeScriptTodo 工程中,创建Validators.ts文件;
2. 定义一个 ValidatableTodo 类,以前好像只定义了Todo 接口,还没有定义过相关的类。这个类带有“验证”功能,我们希望验证Todo的属性是否合法;
3. “验证” 功能本身不在class内部实现,而是作为一个decorator,可以让任何类具有“验证” 功能;
4. function validatable(target: Function) 就是一个class decorator,它的参数列表必须是这样;
5. 后面两个interface起辅助作用,并不是定义decorator必需的。
2. 再添加两个interface
export interface ValidatableTodo extends IValidatable {
}
export interface IValidatable {
validate(): IValidationResult[];
}
这两个interface也不是class decorator必需的,只是为了提供类型信息。
3. 使用validate( )
//TodoService.ts
import { ValidatableTodo } from './Validators';
export default class TodoService implements ITodoService {
// ...
add(input) {
let todo = new ValidatableTodo();
todo.id = generateTodoId();
todo.state = TodoState.Active;
if(typeof input === 'string') {
todo.name = input;
}
else if(typeof input.name === 'string') {
todo.name = input.name;
} else {
throw 'Invalid Todo name!';
}
let errors = todo.validate();
if(errors.length) {
let combinedErrors = errors.map(x => `${x.property}: ${x.message}`);
throw `Invalid Todo! ${combinedErrors}`;
}
this.todos.push(todo);
return todo;
}
//...
}
说明:
1. 第9行,使用 ValidatableTodo;
2. 第22行,验证。
现在,ValidatableTodo 的 _validators 还是空的,不会做任何验证,下一节开始添加。
3. Property Decorators
给 name 属性添加一个decorator: required
@validatable
export class ValidatableTodo implements Todo {
id: number;
@required
name: string;
state: TodoState;
}
function required(target: Object, propertyName: string) {
let validatable = <{_validators: IValidator[]}>target;
let validators = (validatable._validators || (validatable._validators = []));
validators.push(function(instance){
let propertyValue = instance[propertyName];
let isValid = propertyValue != undefined;
if(typeof propertyValue === 'string') {
isValid = propertyValue && propertyValue.length > 0;
}
return {
isValid,
message: `${propertyName} is required`,
property: propertyName
};
});
}
这就是 property decorator。
在Chrome的console 中运行如下代码:
System.import('Validators').then(function(module) {window.ValidatableTodo = module.ValidatableTodo});
var todo = new ValidatableTodo();
todo.validate();
注意,System.import 使用promise,是异步运行的,所以,等第一段代码运行完,再执行第二段。
看结果:
由于我们新创建的todo的name为空,validate() 会返回一个错误。
执行如下代码,再看看validate() 结果:
todo.name = "Rinse my hair";
todo.validate();
4. Decorator Factories
decorator 的参数列表是固定,如上面的 log(), valitable(), required() ,它们所带的参数都是“待修饰对象” 的相关属性。如果我们希望用一个正则表达式来验证 todo.name,怎么把这个正则表达式传送进去呢?使用decorator factory。
顾名思义,decorator factory 是一个函数,它返回一个decorator。
@validatable
export class ValidatableTodo implements Todo {
id: number;
@required
@regex(`^[a-zA-Z ]*$`)
name: string;
state: TodoState;
}
function regex(pattern: string, flags?: string) {
let expression = new RegExp(pattern, flags);
return function(target: Object, propertyName: string) {
let validatable = <{_validators: IValidator[]}>target;
let validators = (validatable._validators || (validatable._validators = []));
validators.push(function(instance){
let propertyValue = instance[propertyName];
let isValid = expression.test(propertyValue);
return {
isValid,
message: `${propertyName} does not match ${expression}`,
property: propertyName
}
});
};
}
说明:
1. 第12行,定义了一个regex() 函数,它是decorator factory;
2. 第14行,返回一个property decorator,注意看它的参数列表,符合property decorator的定义;
3. 第6行,用regex修饰name;
4. property可以有多个decorator,function和class也不例外。
到Chrome的console中试一下:
说明:
1. 第一次,todo.name = “Rinse my hair!” ,字符串中有个感叹号,不符合regex;
2. 第二次,去掉感叹号,validate() 返回的errors为空。