What is metaprogramming

Metaprogramming, in plain English, extends the capabilities of the program itself. Common code operates on data, and metaprogramming operates on other code.

For example, 🌰 : 3D printers can print everything from tables and chairs to models and even handguns. But there is one special 3D printer that can print “3D printers”! This 3D printer can be called a meta-3D printer!

reflect-metadata

In order to enhance typescript’s metaprogramming capabilities, ES7 makes a proposal for Metadata, which is used to add and read Metadata when declaring objects or object properties. Reflect-metadata is currently the SHIM implementation of this proposal.

Declarative apis

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!

Copy the code

Imperative 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);
 
Copy the code

Three metadata key values defined in typescript

  • Type metadata uses the metadata key “Design :type”.
  • Parameter type metadata uses the metadata key “Design: Paramtypes”.
  • Returntype metadata using the metadata key “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 {
    return1}}Copy the code

application

Implementation of Controller and Get decorators

In nodeJS frameworks commonly used in the industry, such as midwayJs and nestJs, we can find that the controller class is basically defined as follows. We can see that we use decorators Get and Controller in 🌰 to simplify the definition of routing information. And strong binding of routing information to routing processing functions. Reflect-metadata can be used to help us implement this logic

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'}}})Copy the code

Below is a simple implementation of the decorator. You can see that the Controller decorator will define routing information on the class. The Get and Post decorators define method types and routing information on specific routing functions

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')
Copy the code

Now, for a Controller class file, the routing handler functions defined on and in the class already carry routing meta-information. When we start a KOA-BASED NodeJS application, we need to scan the Controller class file and register the routing handler functions

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'} // Register the route handler functionfunctionregisterUrl(router: Router, prefixRoute: string, target: Object) {const prototype = object.getProtoTypeof (target) 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(' Register get request:${finalRoute}`)}else if (method === 'POST') {console.log(' Register POST request:${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 (letFile of tsFiles) {console.log(colors.blue(' start processing controller:${file}') // Const controllerClass = require(path.join(dir, dir) Const prefixRoute = reflect.getmetadata (META_PATH, controllerClass.default) registerUrl(router, prefixRoute, new controllerClass.default()) } }exportdefault (dir? : string) => { const real_dir = dir || path.resolve(__dirname,'.. /controllers')
  readControllerFile(router, real_dir)
  return router.routes()
}

Copy the code

Dependency injection

As the business logic becomes more complex, in order to keep the individual Controller files clean, we choose to split the business logic into service classes, and then the Controller class looks like this

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()
  }
}

Copy the code

Then we need to automatically create the required service class instance for injection when we create the current Controller class instance. The logic is as follows

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

  for (letFile of tsFiles) {console.log(colors.blue(' start processing controller:${file}') // Const controllerClass = require(path.join(dir, dir) Const prefixRoute = reflect.getmetadata (META_PATH, Controllerclass.default) // Get a list of injected service classes'design:paramtypes', controllerClass.default) // instantiate service const args = providers?. Map ((provider: Constructor) => new provider()) || [] registerUrl(router, prefixRoute, new controllerClass.default(... args)) } } ...Copy the code

Instead of explicitly using providers and Inject decorators, we use the metadata key design: Paramtypes defined in typescript

Reference documentation

  • Reflect – metadata documents: www.npmjs.com/package/ref…
  • The Metadata Proposal: rbuckton. Making. IO/reflect – met…
  • Reflect – metadata making: github.com/rbuckton/re…
  • Blog.wolksoftware.com/decorators-…
  • Midwayjs: github.com/midwayjs/mi…