Typescript 元编程简述及应用

2,416 阅读4分钟

什么是元编程

元编程,通俗点说就是,可以扩展程序自身的能力。一般代码的操作对象是数据,元编程操作的对象是其它代码。

类比一下,举个🌰: 3D打印机可以打印各种东西,什么桌椅,模型啦,甚至是手枪。但是有个3D打印机比较特别,它能打印“3D打印机”!这个3D打印机就可以称为“元3D打印机”!

reflect-metadata

为了给 typescript 增强元编程的能力,ES7出了一个关于Metadata的提案,它主要用来在声明对象或对象属性的时候添加和读取元数据,reflect-metadata 目前是该提案的shim实现。

声明式API

import 'reflect-metadata'

@Reflect.metadata('classExtension', 'This is a class extension!')
class Test {
  @Reflect.metadata('methodExtension', 'This is a method extension!')
  public hello(): string {
    return 'hello world'
  }
}

console.log(Reflect.getMetadata('classExtension', Test)) // This is a class extension!
console.log(Reflect.getMetadata('methodExtension', new Test(), 'hello')) // This is a method extension!

命令式API

// define metadata on an object or property
Reflect.defineMetadata(metadataKey, metadataValue, target);
Reflect.defineMetadata(metadataKey, metadataValue, target, propertyKey);
 
// check for presence of a metadata key on the prototype chain of an object or property
let result = Reflect.hasMetadata(metadataKey, target);
let result = Reflect.hasMetadata(metadataKey, target, propertyKey);
 
// check for presence of an own metadata key of an object or property
let result = Reflect.hasOwnMetadata(metadataKey, target);
let result = Reflect.hasOwnMetadata(metadataKey, target, propertyKey);
 
// get metadata value of a metadata key on the prototype chain of an object or property
let result = Reflect.getMetadata(metadataKey, target);
let result = Reflect.getMetadata(metadataKey, target, propertyKey);
 
// get metadata value of an own metadata key of an object or property
let result = Reflect.getOwnMetadata(metadataKey, target);
let result = Reflect.getOwnMetadata(metadataKey, target, propertyKey);
 
// get all metadata keys on the prototype chain of an object or property
let result = Reflect.getMetadataKeys(target);
let result = Reflect.getMetadataKeys(target, propertyKey);
 
// get all own metadata keys of an object or property
let result = Reflect.getOwnMetadataKeys(target);
let result = Reflect.getOwnMetadataKeys(target, propertyKey);
 
// delete metadata from an object or property
let result = Reflect.deleteMetadata(metadataKey, target);
let result = Reflect.deleteMetadata(metadataKey, target, propertyKey);
 

3个在typescript中定义的元数据键值

  • 类型元数据使用元数据键"design:type"。
  • 参数类型元数据使用元数据键"design:paramtypes"。
  • 返回类型元数据使用元数据键"design:returntype"。
function logType(target: any, key: string) {
  console.log(0, target, key)
  const t = Reflect.getMetadata('design:type', target, key)
  console.log(`${key} type: ${t.name}`)
}

function logParamTypes(target: any, key: string) {
  console.log(1, target, key)
  const types = Reflect.getMetadata('design:paramtypes', target, key)
  console.log(`${key} param types: ${types}`)
}

function logReturnTypes(target: any, key: string) {
  console.log(2, target, key)
  const types = Reflect.getMetadata('design:returntype', target, key)
  console.log(`${key} return types: ${types}`)
}


class Demo01 {
  @logType // apply property decorator
  public attr1: string | undefined
}

class Foo {}
interface IFoo {}

class Demo02 {
  @logParamTypes // apply parameter decorator
  @logReturnTypes
  doSomething(
    param1: string,
    param2: number,
    param3: Foo,
    param4: { test: string },
    param5: IFoo,
    param6: Function,
    param7: (a: number) => void
  ): number {
    return 1
  }
}

应用

Controller与Get装饰器的实现

现在业界常用的nodejs框架如midwayJs, nestJs,我们会发现其controller类的定义方式基本上都如下,可以看到我们在🌰中使用了装饰器Get, Controller来简化路由信息的定义,以及把路由信息和路由处理函数强绑定。reflect-metadata 可以用来帮助我们实现这一逻辑

import { Context, Next } from 'koa'
import { Get, Controller } from '../decorator'

@Controller('/')
export default class HomeController {
  @Get('home')
  async visitHome(ctx: Context, next: Next) {
    ctx.render('index.html', { title: 'welcome' })
  }
}

下面是装饰器的简单实现,可以看到controller装饰器将会把路由信息定义在类上。而 Get, Post 装饰器将会把方法类型和路由信息定义在具体的路由处理函数上

export const META_METHOD = 'method'
export const META_PATH = 'path'

const createMethodDecorator =
  (method: string) =>
  (path: string): MethodDecorator => {
    return (target, key, descriptor) => {
      Reflect.defineMetadata(META_PATH, path, descriptor.value as any)
      Reflect.defineMetadata(META_METHOD, method, descriptor.value as any)
    }
  }

export const Controller = (path: string): ClassDecorator => {
  return (target) => {
    Reflect.defineMetadata(META_PATH, path, target)
  }
}
export const Get = createMethodDecorator('GET')
export const Post = createMethodDecorator('POST')

现在对于一个controller类文件,类上和类中定义的路由处理函数都已经带上了路由相关的元信息,我们在起一个基于koa的nodejs应用时,需要去扫描这些controller类文件,并注册这些路由处理函数

import fs from 'fs'
import path from 'path'
import Router from '@koa/router'
import { META_PATH, META_METHOD } from '../decorator'
import colors from 'colors'

const router = new Router()

function isConstructor(target: string) {
  return target === 'constructor'
}

function isFunction(target: any) {
  return typeof target === 'function'
}
// 注册路由处理函数
function registerUrl(router: Router, prefixRoute: string, target: object) {
  const prototype = Object.getPrototypeOf(target)
  // 筛选出 controller 中定义的方法
  const methodsNames = Object.getOwnPropertyNames(prototype).filter(
    (item) => !isConstructor(item) && isFunction(prototype[item])
  )

  methodsNames.forEach((methodName) => {
    const fn = prototype[methodName]

    const route = Reflect.getMetadata(META_PATH, fn)
    const method = Reflect.getMetadata(META_METHOD, fn)
    const finalRoute = path.join(prefixRoute, route)

    if (method === 'GET') {
      router.get(finalRoute, fn)
      console.log(`注册 GET 请求:${finalRoute}`)
    } else if (method === 'POST') {
      console.log(`注册 POST 请求:${finalRoute}`)
      router.post(finalRoute, fn)
    } else {
      console.log(`invalid URL: ${finalRoute}`)
    }
  })
}

function readControllerFile(router: Router, dir: string) {
  const files = fs.readdirSync(dir)
  const tsFiles = files.filter((item) => item.endsWith('.ts'))

  for (let file of tsFiles) {
    console.log(colors.blue(`开始处理controller:${file}`))
    // 自定义的 controller 类
    const controllerClass = require(path.join(dir, file))
    // 获取前置路由
    const prefixRoute = Reflect.getMetadata(META_PATH, controllerClass.default)
    registerUrl(router, prefixRoute, new controllerClass.default())
  }
}

export default (dir?: string) => {
  const real_dir = dir || path.resolve(__dirname, '../controllers')
  readControllerFile(router, real_dir)
  return router.routes()
}

依赖注入

随着业务逻辑越来越复杂,为了保持单个controller文件的clean, 我们会选择把业务逻辑抽成一个个的service类,然后controller类变成如下样子

import { Context, Next } from 'koa'
import { Get, Controller } from '../decorator'
import RegionCodeService from '../services/RegionCodeService'

@Controller('/user')
export default class UserController {
  constructor(public readonly regionCodeService: RegionCodeService) {}

  @Get('/getRegionCode')
  async getRegionCode(ctx: Context, next: Next) {
    ctx.body = this.regionCodeService.getRegionCode()
  }
}

然后我们需要在创建当前controller类实例的时候,自动创建好需要的 service 类实例进行注入,逻辑如下

...
function readControllerFile(router: Router, dir: string) {
  const files = fs.readdirSync(dir)
  const tsFiles = files.filter((item) => item.endsWith('.ts'))

  for (let file of tsFiles) {
    console.log(colors.blue(`开始处理controller:${file}`))
    // 自定义的 controller 类
    const controllerClass = require(path.join(dir, file))
    // 获取前置路由
    const prefixRoute = Reflect.getMetadata(META_PATH, controllerClass.default)
    // 获取注入的service 类列表
    const providers = Reflect.getMetadata('design:paramtypes', controllerClass.default)
    // 实例化 service
    const args = providers?.map((provider: Constructor) => new provider()) || []

    registerUrl(router, prefixRoute, new controllerClass.default(...args))
  }
}
...

这里没有显式的使用Provider, Inject装饰器,而是直接使用 typescript 里定义好了的元数据键值 design:paramtypes

参考文档