本文最后更新于 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) 
 
  |