准备工作
npm init -y
npm i --save-dev gulp gulp-typescript typescript
npm i --save reflect-metadata
gulpfile.js
const gulp = require('gulp')
const tsc = require('gulp-typescript')
const typescript = require('typescript')
const tsProject = tsc.createProject({
removeComments: false,
noImplicitAny: false,
target: 'es5',
module: 'commonjs',
declarationFiles: false,
emitDecoratorMetadata: true,
typescript: typescript
})
gulp.task('buil-source', () => {
return gulp.src(`${__dirname}/file.ts`)
.pipe(tsc(tsProject))
.js.pipe(gulp.dest(`${__dirname}/`))
})
注解和装饰器
注解是一种为类声明添加元数据的方法。然后,元数据就可以被诸如依赖注入容器这样的工具所使用。
装饰器,用来在代码设计时注释和修改类和类的属性。已成为ES7标准的特性。
class Person {
public name: string
public surname: string
constructor(name: string, surname: string) {
this.name = name
this.surname = surname
}
public saySomething(something: string): string {
return `${this.name} ${surname} says: ${something}`
}
}
可供使用的装饰器一共有4种,分别用来装饰:类、属性、方法和参数
类装饰器
类装饰器是指接受一个类构造函数作为参数的函数,并且返回undefined、参数中提供的构造函数或一个新的构造函数。返回undefined等同于返回参数中提供的构造函数。
类装饰器用来修改类的构造函数。如果装饰器函数返回undefined,那么类仍然使用原来的构造函数。如果装饰器有返回值,那么返回值会被用来覆盖原来的构造函数。
创建一个装饰器
function logClass(target: any){
//...
}
使用装饰器装饰一个类
@logClass
class Person {
public name: string
public surname: string
//...
}
编译结果
var Person = (function() {
function Person(name, surname){
this.name = name
this.surname = surname
}
Person.prototype.saySomething = function(something) {
return `${this.name} ${surname} says: ${something}`
}
Person = _decorate([
logClass
], Person)
return Person
})()
装饰器用来为元素添加一些额外的逻辑或元数据。当我们想拓展一个函数的功能时,需要往原函数上包一个新函数,新函数里有额外的逻辑,且能执行原函数里的方法。
function logClass(target: any){
//保存原构造函数的引用
const original = target
// 用来生成类的实例的工具方法
function construct(constructor, args) {
const c: any = function() {
return constructor.apply(this, args)
}
c.prototype = constructor.prototype
return new c()
}
// 新的构造函数行为
const f: any = function(...args) {
console.log(`New: ${original.name}`)
return construct(original, args)
}
// 复制原型,使instanceof操作能正常使用
f.prototype = original.prototype
//返回新的构造函数(将会覆盖原构造函数)
return f
}
哎装饰了类的构造函数后,会有
var me = new Person('Remo', 'Jansen') // 输出"New: Person"
方法装饰器
方法装饰器和类装饰器十分相似,但是他用来覆盖类的方法。
//...
@logMethod
public saySomething(something: string) : string {
return `${this.name} ${surname} says: ${something}`
}
方法装饰器被调用时,带有以下参数:
- 包含了被装饰方法的类的原型。即Person.prototype
- 被装饰方法的名字,即saySomething
- 被装饰方法的属性描述对象,即Object
function logMethod(target: any, key: string, descriptor: any) {
// 保存原方法的引用
const originalMethod = descriptor.value
// 编辑descriptor参数的value属性
descriptor.value = function(...args: any[]){
// 将方法参数转换为字符串
const a = args.map(a => JSON.stringify(a)).join()
// 执行方法得到其返回值
const result = originalMethod.apply(this, args)
// 将返回值转化为字符串
const r = JSON.stringify(result)
// 将函数的调用细节打印在控制台中
console.log(`Call:${key}(${a}) => ${r}`)
// 返回方法的调用结果
return result
}
// 返回编辑后的属性描述对象
return descriptor
}
运行结果
const me = new Person('Remo', 'Jansen')
me.saySomething('hello!')
//Call: saySomething('hello') => 'Remo Jansen says: hello!'
属性装饰器
属性装饰器和方法装饰器十分相似。主要区别在于,一个属性装饰器没有返回值且没有第三个参数(属性描述对象)
class Person {
@logProperty
public name: string
}
使用一个新属性来替代原来的属性,新属性会表现得与原属性一致,除了在更改时会将改变的值打印在控制台中
function logProperty(target: any, key: string) {
// 属性值
const _val = this[key]
// 属性的getter
const getter = function() {
console.log(`Get:${key} => ${_val}`)
return _val
}
// 属性的setter
const setter = function(newVal) {
console.log(`Set:${} => ${newVal}`)
_val = newVal
}
// 删除属性,在严格模式下,如果对象是不可配置的,
// 删除操作将会抛出一个错误。在非严格模式下,则会返回false
if(delete this[key]) {
Object.defineProperty(target, key, {
get: getter,
set: setter,
enumerable: true,
configurable: true
})
}
}
使用属性装饰器
const me = new Person('Remo', 'Jansen')
// Set: name => Remo
me.name = 'Remo H.'
// Set: name => Remo H.
const n = me.name
// Get: name Remo H.
参数装饰器
参数装饰器函数是一个接收三个参数的函数:一个包含了被装饰参数的方法的对象、方法的名字(或undefined)和参数在参数列表中的索引。装饰器的返回值将被忽略。
public saySomething(@addMetadata something: string) : string {
return `${this.name} ${this.surname} says: ${something}`
}
参数属性没有返回值,意味不能覆盖整个包含被修饰参数的方法
function addMetadata(target: any, key: string, index: number) {
const metadataKey = `_log_${key}_parameters`
if(Array.isArray(target[metadataKey])) {
target[metadataKey].push(index)
} else {
target[metadataKey] = [index]
}
}
单独的参数装饰器并不是很有用,他需要和方法装饰器结合,参数装饰器用来添加元数据,然后通过方法装饰器来读取它。
@readMetadata
public saySomething(@addMetadata something : string) : string {
return `${this.name} ${surname} says: ${something}`
}
打印被装饰的参数
function readMetadata(target: any, key: string, descriptor: any) {
const originalMethod = descriptor.value
descriptor.value = function(...args: any[]){
const metadataKey = `_log_${key}_parameters`
const indices = target[metadataKey]
if(Array.isArray(indices)) {
for(let i = 0; i < args.length; i++) {
if (indices.indexOf(i) !== -1) {
const arg = args[i]
const argStr = JSON.stringify(arg) || arg.toString()
console.log(`${key} arg[${i}]:${argStr}`)
}
}
const result = originalMethod.apply(this, args)
return result
}
}
return descriptor
}
执行
const person = new Person('Remo', 'Jansen')
person.saySomething('hello')
// saySomething arg[0]: 'hello!'
装饰器工厂
装饰器工厂是一个接收任意数量参数的函数;并且必须返回上述的任意一种装饰器。
可以使用装饰器工厂来使装饰器更容易被使用
@logClass
class Person {
@logProperty
public name: string
@logMethod
public saySomething(@logParameter something: string): string {
return `${this.name} ${surname} says: ${something}`
}
}
通用装饰器
@log
class Person {
@log
public name: string
@log
public saySomething(@log something: string): string {
return `${this.name} ${surname} says: ${something}`
}
}
实现
function log(...args: any[]) {
switch(args.length) {
case 1:
return logClass.apply(this, args)
case 2:
// 由于属性装饰器没有返回值
// 所以使用break取代return
logProperty.apply(this, args)
case 3:
if(typeof args[2] === 'number') {
logParameter.apply(this, args)
}
return logMethod.apply(this, args)
default:
throw new Error('Decorators are not valid here!')
}
}
带有参数的装饰器
可以使用一种特殊的装饰器工厂来配置装饰器的行为。
@logClass('option')
class Person {
//...
}
为了给装饰器传递参数,需要使用一个函数来包裹装饰器。这个包裹函数接收参数并返回一个装饰器
function logClass(option: string) {
return function(target: any) {
// 类装饰器的逻辑
// 可以访问到装饰器的参数
console.log(target, option)
}
}
反射元数据API
不久后,TS团队决定使用反射元信息API来替代这些保留装饰器。他的思想与使用保留装饰器十分相似,但使用了反射元信息API代替保留装饰器,来获取元信息。TS文档中定义了三种保留元数据键:
- 类型元数据使用元数据键 design:type
- 参数类型元数据使用元数据键 design:paramtypes
- 返回值元数据使用元数据键 design: returntype
使用,引用并导入一个包
///<reference path='./node_modules/reflect-metadata/reflect-metadata.d.ts'/>
import 'reflect-metadata'
为了测试,创建一个类,而在运行时,去获取类的一个属性的类型。
class Demo {
@logType
public attr1: string
}
调用Reflect.getMetadata()方法并传入design:type键,而不是使用保留装饰器@type来获取属性类型。
function logType(target: any, key: string) {
var t = Reflect.getMetadata('design:type', target, key)
console.log(`${key} type: ${t.name}`)
}
输出
'attr1 type: String'