什么是元编程
元编程,通俗点说就是,可以扩展程序自身的能力。一般代码的操作对象是数据,元编程操作的对象是其它代码。
类比一下,举个🌰: 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
参考文档
- reflect-metadata文档:www.npmjs.com/package/ref…
- Metadata Proposal: rbuckton.github.io/reflect-met…
- reflect-metadata github: github.com/rbuckton/re…
- blog.wolksoftware.com/decorators-…
- midwayjs: github.com/midwayjs/mi…