2-2 TS 实战

163 阅读5分钟

原文链接(格式更好):《2-2 TS 实战》

元数据

定义:描述数据的数据

通过给类、方法指定/定义属性进一步丰富它的形态

元数据的使用范围通常为对象、类、方法

作用:

  • 扩展已有的属性形态
  • 不改变本身的代码逻辑

场景举例:

在实际业务中,存在老业务的迭代或扩展,这种情况下可以使用元数据进行扩展

// 老业务:course 函数返回一个字符串,代表课程名称
let course = function() {
  return 'ts 实战'
}

// 新业务:course 函数要拥有课程时长、上课老师等属性

扩展方法:

  1. 采用原型链的思路来实现,通过 Function.prototype 实现
    1. 隐蔽性太高,不易查找
    2. 维护成本大,协作效率低
    3. 对象的操作不统一

在 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"
);

小细节

当设置元数据的时候,可以有两种写法:

  1. 函数调用形式:Reflect.defineMetadata(...)
  2. 装饰器形式:@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

扩展知识

元编程

官方文档:元编程 - JavaScript | MDN

使用Reflect 和 Proxy,可以实现元级别的编程(可自定义基本语言操作(例如属性查找、赋值、枚举和函数调用等))

Reflect

官方文档:Reflect - JavaScript | MDN

Proxy

官方文档:Proxy - JavaScript | MDN

用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等),并且对应的操作也会转发到这个对象上

语法: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 }