原文链接(格式更好):《2-2 TS 实战》
元数据
定义:描述数据的数据
通过给类、方法指定/定义属性进一步丰富它的形态
元数据的使用范围通常为对象、类、方法
作用:
- 扩展已有的属性形态
- 不改变本身的代码逻辑
场景举例:
在实际业务中,存在老业务的迭代或扩展,这种情况下可以使用元数据进行扩展
// 老业务:course 函数返回一个字符串,代表课程名称
let course = function() {
return 'ts 实战'
}
// 新业务:course 函数要拥有课程时长、上课老师等属性
扩展方法:
- 采用原型链的思路来实现,通过 Function.prototype 实现
-
- 隐蔽性太高,不易查找
- 维护成本大,协作效率低
- 对象的操作不统一
在 JS 中,对象的操作一直都是不统一的
// 创建对象
let obj = {}
// 新增属性
obj.name = 'lisi'
// 更改属性
obj.name = '张三'
// 删除属性
delete obj.name
// 获取属性
obj.name
// 合并对象
Object.assign(obj1, obj2)
// 可以发现,对象的不同操作语法是各式各样的
在 TS 里面,就有一种统一的操作对象的方式:Reflect
元数据的实现是通过Reflect + metadata
Reflect
官方文档:Reflect - JavaScript | MDN
一种统一的操作对象的方式
const obj = {}
// 新增/更改属性;参数:目标对象,属性名称,属性描述;返回值:boolean(是否操作成功)
// Reflect.defineProperty(target, propertyKey, attributes)
Reflect.defineProperty(obj, 'name', { value: 'lisi' }) // true
Reflect.defineProperty(obj, 'age', { value: 27 }) // true
// obj: { name: 'lisi', age: 27 }
// 其中的 name|age 是不可操作的(configurable 默认为 false)
// 新增/更改属性;参数:目标对象,属性名称,属性值;返回值:boolean(是否操作成功)
// Reflect.set(target, propertyKey, value)
Reflect.set(obj, 'address', '四川成都')
// obj: { name: 'lisi', age: 27, address: '四川成都'}
// 其中的 address 是可操作的(configurable 默认为 true)
// 获取属性;参数:目标对象,属性名称;返回值:属性的值
// Reflect.get(target, propertyKey)
Reflect.get(obj, 'name') // 'lisi'
// 删除属性;参数:目标对象,属性名称;返回值:boolean(是否操作成功)
// Reflect.deleteProperty(target, propertyKey)
Reflect.deleteProperty(obj, 'age') // false
Reflect.deleteProperty(obj, 'address') // true
// obj: { name: 'lisi', age: 27}
这样对象的所有操作都统一使用Reflect来完成:
增(Reflect.set(...))、删(Reflect.deleteProperty(...))、改(Reflect.set(...))、查(Reflect.get(...))
Metadata
在 TS 中元数据的具体实现,需要引入一个第三方库
再次强调元数据的使用范围可以为对象、类、属性(变量/方法)
import 'reflect-metadata'
用的前提是:tsconfig.json 开启配置
{
"compilerOptions" : {
"emitDecoratorMetadata": true
}
}
设置
Reflect.defineMetadata(
metadataKey: any,
metadataValue: any,
target: Object,
propertyKey: string | symbol
): void
// metadataKey:存储 metadata 的 Key,require
// metadataValue:存储 metadata 的 值,require
// target:metadata 要绑的目标对象,require
// propertyKey:目标对象的属性名,optional
class Test {
static oldName = "zhangsan";
static sayYes() {
return "好的";
}
name = "lisi";
sayHello() {
return `你好,我是${this.name}`;
}
}
Reflect.defineMetadata("Test_metadataKey1", "Test_metadataValue1", Test);
Reflect.defineMetadata(
"Test_public_metadataKey2",
"Test_public_metadataValue2",
Test,
"name"
);
Reflect.defineMetadata(
"Test_static_metadataKey2",
"Test_static_metadataValue2",
Test,
"oldName"
);
Reflect.defineMetadata(
"Test_public_metadataKey3",
"Test_public_metadataValue3",
Test,
"sayHello"
);
Reflect.defineMetadata(
"Test_static_metadataKey4",
"Test_static_metadataValue4",
Test,
"sayYes"
);
小细节
当设置元数据的时候,可以有两种写法:
函数调用形式:Reflect.defineMetadata(...)装饰器形式:@Reflect.metatda(...)
这两种写法都能设置元数据,但针对装饰器形式设置的,对应的获取时就存在一些注意事项
@Reflect.metadata("Test_metadataKey1", "Test_metadataValue1")
class Test {}
// @Reflect.metadata 的写法就是 2-1 TS 详解里面的"类装饰器",其原理代码大致如下:
Reflect.metadata = function(metadataKey: string, metadataValue: any) {
return function(target: Object, propertyKey: string) {
// 具体的逻辑暂且忽略.........
}
}
操作类本身
设置
// Reflect.defineMetadata(...) 设置
class Test {}
Reflect.defineMetadata("Test_metadataKey1", "Test_metadataValue1", Test);
// @Reflect.metadata(...) 设置
@Reflect.metadata("Test_metadataKey1", "Test_metadataValue1")
class Test {}
// 以上两种方式是等价的,都是设置元数据到"类"
取值
// 相同取法
Reflect.getMetadata("Test_metadataKey1", Test); // "Test_metadataValue1"
操作类属性
设置
// Reflect.defineMetadata(...) 设置
class Test {
static oldName = 'zhangsan'
name = "lisi"
}
Reflect.defineMetadata(
"Test_public_metadataKey2",
"Test_public_metadataValue2",
Test,
"name"
);
Reflect.defineMetadata(
"Test_static_metadataKey2",
"Test_static_metadataValue2",
Test,
"oldName"
);
// @Reflect.metadata(...) 设置
class Test {
@Reflect.metadata("Test_static_metadataKey2", "Test_static_metadataValue2")
static oldName = "zhangsan";
@Reflect.metadata("Test_public_metadataKey2", "Test_public_metadataValue2")
name = "lisi";
}
// 咋一看操作是一样的,但其取值对象是不一样
给类的静态属性定义了元数据,取值target为类本身
给类的动态属性定义了元数据,取值target为类的实例
取值
Reflect.getMetadata(
"Test_static_metadataKey2",
Test, // 这里就必须为类,若为类的实例,则取值结果为 undefined
"oldName"
); // "Test_static_metadataValue2"
Reflect.getMetadata(
"Test_public_metadataKey2",
new Test(), // 这里就必须为类的实例,若为类,则取值结果为 undefined
"name"
); // "Test_public_metadataValue2"
获取
Reflect.getMetadata(
metadataKey: any,
target: Object,
propertyKey: string | symbol
): any
// metadataKey:存储 metadata 的 Key,require
// target:metadata 要绑的目标对象,require
// propertyKey:目标对象的属性名,optional
Reflect.getMetadata("Test_metadataKey1", Test); // "Test_metadataValue1"
Reflect.getMetadata("Test_public_metadataKey2", Test, "name"); // "Test_public_metadataValue2"
Reflect.getMetadata("Test_static_metadataKey2", Test, "oldName"); // "Test_static_metadataValue2"
Reflect.getMetadata("Test_public_metadataKey3", Test, "sayHello"); // "Test_public_metadataValue3"
删除
Reflect.deleteMetadata(
metadataKey: any,
target: Object,
propertyKey: string | symbol
): boolean
// metadataKey:存储 metadata 的 Key,require
// target:metadata 要绑的目标对象,require
// propertyKey:目标对象的属性名,optional
Reflect.deleteMetadata("Test_metadataKey1", Test); // true
Reflect.deleteMetadata("Test_public_metadataKey2", Test, "name"); // true
Reflect.deleteMetadata("Test_static_metadataKey2", Test, "oldName"); // true
Reflect.deleteMetadata("Test_public_metadataKey3", Test, "sayHello"); // true
通过上面的设置、获取、删除方法,已基本了解了metadata的使用,并且也成功的在不改动原本数据的情况下,扩展了新的属性与值
实战
服务端场景 - 路由封装
// app.ts
// 一个简单的 app.ts
import express from "express";
const app = express();
const port = 3000;
app.get("/", (req, res) => {
res.send("Hello World!");
});
app.get("/xxxx", (req, res) => {
res.send("xxxx");
});
app.listen(port, () => {
console.log(`Server is running at http://localhost:${port}`);
});
按照上述的写法,服务端的路由将会无比的多,并且臃肿和不好管理
封装思路:使用面向对象写法,进行更好的归类
// user.ts
// 封装用户相关的接口
import { Get, Post } from "../decorators/methods";
import { Path } from "../decorators/path";
export class User {
// 用户查询
@Get
@Path("/user/info")
info() {
// 接口逻辑...
return "info";
}
// 用户登录
@Post
@Path("/user/login")
login() {
// 接口逻辑...
return "login";
}
logout() {
// 接口逻辑...
return "logout";
}
}
// decorators/methods.ts
export const methodsKey = Symbol("router:methods");
export const Get = (target: Object, propertyKey: string) => {
Reflect.defineMetadata(methodsKey, "get", target, propertyKey);
};
export const Post = (target: Object, propertyKey: string) => {
Reflect.defineMetadata(methodsKey, "post", target, propertyKey);
};
// decorators/path.ts
import { Request, Response } from "express";
export const pathKey = Symbol("router:path");
// Path 装饰器 - 用在 类的方法 上
export const Path = (path: string): Function => {
return (
target: Object,
propertyKey: string,
desicriptor: PropertyDescriptor
) => {
Reflect.defineMetadata(pathKey, path, target, propertyKey);
// 1、获取方法本身,存一份
const oldMethod = desicriptor.value;
// 2、边缘检测:无值则直接 return
if (!oldMethod) return;
// 3、覆盖原来的方法
desicriptor.value = function (req: Request, res: Response) {
// a. 获取到原方法的参数
const params = Object.assign({}, req.body, req.query);
// b. 触发原方法的调用
const result = oldMethod.call(this, params);
// c. 返回给客户端
res.send(result);
};
};
};
// router.ts
import { User } from "./user";
import { methodsKey } from "../decorators/methods";
import { pathKey } from "../decorators/path";
export default (app: any) => {
const user = new User();
Object.keys(User).forEach((key) => {
// app.get(path, fun)
const method = Reflect.getMetadata(methodsKey, user, key);
const path = Reflect.getMetadata(pathKey, user, key);
app[method](path, user); // 挂载完成路由监听
});
};
最终实现的user.ts写法已经跟现在的 nodejs 框架nest.js、midday.js等类似了
客户端场景 - 倒计时器
// 需求:写一个自定义时间的倒计时器
// 比如:从现在起到明天这个时间点的倒计时,11:59:59
class Countdown {
// endTime: 倒计时的终点时间
// step: 倒计时间隔,单位毫秒
constructor(endTime: number, step: number){
// 待补充...
}
// 待补充...
}
// 使用
const countdown = new Countdown(Date.now() * 60 * 60 * 12, 1000)
countdown.on('running', time => {
// 只要倒计时还未结束,则该函数每间隔 X(初始化传入的值) 秒后会执行
// time: 剩余时间
// hour: 剩余小时
// minutes: 剩余分钟
// seconds: 剩余秒数
// count: 计时次数
const { hour, minutes, seconds, count } = time
console.log(`还剩:${hour}:${minutes}:${seconds}`)
})
// 打印结果:
// 还剩:11:59:59
// 还剩:11:59:58
// ...
// 还剩:00:09:09
// 需求:写一个自定义时间的倒计时器
// 比如:从现在起到明天这个时间点的倒计时,11:59:59
// 发布订阅用的
import { EventEmitter } from "eventemitter3";
interface CountdownEventMap {
[CountdownEventName.START]: [];
[CountdownEventName.RUNNING]: [RemainTimeData];
[CountdownEventName.STOP]: [];
}
enum CountdownEventName {
START = "start",
STOP = "stop",
RUNNING = "running",
}
enum CountdownStatus {
running,
paused,
stoped,
}
interface RemainTimeData {
hours: number;
minutes: number;
seconds: number;
count: number;
}
class Countdown extends EventEmitter<CountdownEventMap> {
endTime: number;
step: number;
remainTime: number;
count: number;
status: CountdownStatus = CountdownStatus.stoped;
// endTime: 倒计时的终点时间
// step: 倒计时间隔,单位毫秒
constructor(endTime: number, step = 1e3) {
super();
// 待补充...
this.endTime = endTime;
this.step = step;
this.remainTime = 0;
this.count = 0;
this.start();
}
// 待补充...
start() {
this.emit(CountdownEventName.START);
this.status = CountdownStatus.running;
this.countdown();
}
// 计时操作
countdown() {
if (this.status === CountdownStatus.running) {
this.remainTime = Math.max(this.endTime - Date.now(), 0);
this.count++;
this.emit(CountdownEventName.RUNNING, this.calcRemainTimeData());
if (this.remainTime > 0) {
setTimeout(() => {
this.countdown();
}, this.step);
} else {
this.stop();
}
}
}
calcRemainTimeData(): RemainTimeData {
let hours, minutes, seconds, count;
count = this.count;
// 创建一个新的 Date 对象
let date = new Date(this.remainTime);
// 获取小时、分钟和秒
hours = date.getHours();
minutes = date.getMinutes();
seconds = date.getSeconds();
return { hours, minutes, seconds, count };
}
stop() {
this.emit(CountdownEventName.STOP);
this.status = CountdownStatus.stoped;
}
}
// 使用
const countdown = new Countdown(Date.now() * 60 * 60, 1000);
countdown.on(CountdownEventName.RUNNING, (remainTimeData: RemainTimeData) => {
// 只要倒计时还未结束,则该函数每间隔 X(初始化传入的值) 秒后会执行
// remainTimeData: 剩余时间
// hours: 剩余小时
// minutes: 剩余分钟
// seconds: 剩余秒数
// count: 计时次数
const { hours, minutes, seconds, count } = remainTimeData;
console.log(`还剩:${hours}:${minutes}:${seconds}`, count);
});
// 打印结果:
// 还剩:11:59:59
// 还剩:11:59:58
// ...
// 还剩:00:09:09
扩展知识
元编程
使用Reflect 和 Proxy,可以实现元级别的编程(可自定义基本语言操作(例如属性查找、赋值、枚举和函数调用等))
Reflect
官方文档:Reflect - JavaScript | MDN
Proxy
用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等),并且对应的操作也会转发到这个对象上
语法:new Proxy(target, handler)
const obj = {}
const handler = {
// 代理 get 操作(取值)
get(target, propertyKey){
console.log('Proxy get')
return target[propertyKey]
},
// 代理 set 操作(赋值)
set(target, propertyKey, value){
console.log('Proxy set')
target[propertyKey] = value
}
}
const p = new Proxy(obj, handler)
p.a = 1 // 赋值,打印:Proxy set,并且赋值操作也会转发到 obj 上
const a = p.a // 取值,打印:Proxy get
console.log(obj) // { a:1 }