本文最后更新于 2024-03-22T23:32:35+00:00
元数据
定义:描述数据的数据
通过给类、方法指定/定义属性进一步丰富它的形态
元数据的使用范围通常为对象、类、方法
作用:
场景举例:
在实际业务中,存在老业务的迭代或扩展,这种情况下可以使用元数据
进行扩展
1 2 3 4 5 6 7
| let course = function() { return 'ts 实战' }
|
扩展方法:
- 采用原型链的思路来实现,通过 Function.prototype 实现
- 隐蔽性太高,不易查找
- 维护成本大,协作效率低
- 对象的操作不统一
在 JS 中,对象的操作一直都是不统一的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| let obj = {}
obj.name = 'lisi'
obj.name = '张三'
delete obj.name
obj.name
Object.assign(obj1, obj2)
|
在 TS 里面,就有一种统一的操作对象的方式:Reflect
元数据的实现是通过Reflect + metadata
Reflect
官方文档:Reflect - JavaScript | MDN
一种统一的操作对象的方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| const obj = {}
Reflect.defineProperty(obj, 'name', { value: 'lisi' }) Reflect.defineProperty(obj, 'age', { value: 27 })
Reflect.set(obj, 'address', '四川成都')
Reflect.get(obj, 'name')
Reflect.deleteProperty(obj, 'age') Reflect.deleteProperty(obj, 'address')
|
这样对象的所有操作都统一使用Reflect
来完成:
增(Reflect.set(...)
)、删(Reflect.deleteProperty(...)
)、改(Reflect.set(...)
)、查(Reflect.get(...)
)
在 TS 中元数据的具体实现,需要引入一个第三方库
再次强调元数据的使用范围可以为对象、类、属性(变量/方法)
1
| import 'reflect-metadata'
|
用的前提是:tsconfig.json 开启配置
1 2 3 4 5
| { "compilerOptions" : { "emitDecoratorMetadata": true } }
|
设置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| Reflect.defineMetadata( metadataKey: any, metadataValue: any, target: Object, propertyKey: string | symbol ): void
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(...)
这两种写法都能设置元数据
,但针对装饰器形式设置的
,对应的获取时就存在一些注意事项
1 2 3 4 5 6 7 8 9
| @Reflect.metadata("Test_metadataKey1", "Test_metadataValue1") class Test {}
Reflect.metadata = function(metadataKey: string, metadataValue: any) { return function(target: Object, propertyKey: string) { } }
|
操作类
本身
设置
1 2 3 4 5 6 7 8 9
| class Test {} Reflect.defineMetadata("Test_metadataKey1", "Test_metadataValue1", Test);
@Reflect.metadata("Test_metadataKey1", "Test_metadataValue1") class Test {}
|
取值
1 2
| Reflect.getMetadata("Test_metadataKey1", Test);
|
操作类
属性
设置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| 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" );
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
为类的实例
取值
1 2 3 4 5 6 7 8 9 10 11
| Reflect.getMetadata( "Test_static_metadataKey2", Test, "oldName" );
Reflect.getMetadata( "Test_public_metadataKey2", new Test(), "name" );
|
获取
1 2 3 4 5 6 7 8 9 10 11 12 13
| Reflect.getMetadata( metadataKey: any, target: Object, propertyKey: string | symbol ): any
Reflect.getMetadata("Test_metadataKey1", Test); Reflect.getMetadata("Test_public_metadataKey2", Test, "name"); Reflect.getMetadata("Test_static_metadataKey2", Test, "oldName"); Reflect.getMetadata("Test_public_metadataKey3", Test, "sayHello");
|
删除
1 2 3 4 5 6 7 8 9 10 11 12 13
| Reflect.deleteMetadata( metadataKey: any, target: Object, propertyKey: string | symbol ): boolean
Reflect.deleteMetadata("Test_metadataKey1", Test); Reflect.deleteMetadata("Test_public_metadataKey2", Test, "name"); Reflect.deleteMetadata("Test_static_metadataKey2", Test, "oldName"); Reflect.deleteMetadata("Test_public_metadataKey3", Test, "sayHello");
|
通过上面的设置、获取、删除
方法,已基本了解了metadata
的使用,并且也成功的在不改动原本数据的情况下,扩展了新的属性与值
实战
服务端场景 - 路由封装
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
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}`); });
|
按照上述的写法,服务端的路由将会无比的多,并且臃肿和不好管理
封装思路:使用面向对象写法,进行更好的归类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
|
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"; } }
|
1 2 3 4 5 6 7 8 9 10
|
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); };
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
|
import { Request, Response } from "express";
export const pathKey = Symbol("router:path");
export const Path = (path: string): Function => { return ( target: Object, propertyKey: string, desicriptor: PropertyDescriptor ) => { Reflect.defineMetadata(pathKey, path, target, propertyKey);
const oldMethod = desicriptor.value;
if (!oldMethod) return;
desicriptor.value = function (req: Request, res: Response) { const params = Object.assign({}, req.body, req.query);
const result = oldMethod.call(this, params);
res.send(result); }; }; };
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| 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) => {
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
等类似了
客户端场景 - 倒计时器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
|
class Countdown { constructor(endTime: number, step: number){ }
}
const countdown = new Countdown(Date.now() * 60 * 60 * 12, 1000)
countdown.on('running', time => {
const { hour, minutes, seconds, count } = time
console.log(`还剩:${hour}:${minutes}:${seconds}`) })
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123
|
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;
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;
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) => {
const { hours, minutes, seconds, count } = remainTimeData;
console.log(`还剩:${hours}:${minutes}:${seconds}`, count); });
|
扩展知识
元编程
官方文档:元编程 - JavaScript | MDN
使用Reflect 和 Proxy
,可以实现元级别
的编程(可自定义基本语言操作(例如属性查找、赋值、枚举和函数调用等))
Reflect
官方文档:Reflect - JavaScript | MDN
Proxy
官方文档:Proxy - JavaScript | MDN
用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等),并且对应的操作也会转发到这个对象上
语法:new Proxy(target, handler)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| const obj = {}
const handler = { get(target, propertyKey){ console.log('Proxy get') return target[propertyKey] }, set(target, propertyKey, value){ console.log('Proxy set') target[propertyKey] = value } }
const p = new Proxy(obj, handler) p.a = 1 const a = p.a console.log(obj)
|