likes
comments
collection
share

使用TypeScript的装饰器实现简易Nest.js

作者站长头像
站长
· 阅读数 12

在学习Nest.js的过程中, 让我很迷恋的两个功能:

  1. 依赖注入(DI)
  2. 控制器(Controller)

这两个都是依靠TypeScript(简称:ts)强大的装饰器(Decorator), 关于装饰器, 我在之前的文章讲过 TypeScript Decorators装饰器 有兴趣的同学可以去阅读一下, 加深对 装饰器 的理解;

依赖注入

在学习 依赖注入 之前, 我们得先了解什么叫 控制反转(Ioc, Inversion of Control)

维基百科

控制反转(英语:Inversion of Control,缩写为IoC),是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的[耦合度] 其中最常见的方式叫做依赖注入(Dependency Injection,简称DI 还有一种方式叫“依赖查找”(Dependency Lookup)

比如 Class A 中用到了 Class B的对象b, 在平时我们开发的时候,我们需要 去 初始化B的对象, 也就是 new B();

如果采用了 依赖注入之后, 只需要在 A的代码中 定义 一个 B的对象,不需要直接 new 来获取这个对象, 而是通过相关的容器控制程序来将B对象在外部new出来,并注入到A的对象中;

截取Nest.js的一个例子:

import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get('ab*cd')
  getHello(): string {
    return this.appService.getHello();
  }
}

当我们需要 AppService 这个类的对象时, 我们并不需要去创建一个实例, 只需要引入即可, 这样 我们可以在 AppController 的任何地方都可以对这个对象进行操作, 减少了 代码之间的耦合;

简易代码

想起赵本山,宋丹丹的小品, 把大象装进冰箱需要几步? 三步

  • 第一步: 打开冰箱门。

    • tsconfig.json 的配置: "experimentalDecorators": true, "emitDecoratorMetadata": true,

    • 安装 reflect-metadatayarn add reflect-metadata

  • 第二步: 把大象塞进冰箱; 编写代码


import 'reflect-metadata'

interface Contructor<T = any> {
  new (...args: any[]): T
}
function Inject(): ClassDecorator {
  return function (target) {}
}
class AppService {
  sayHello () {
    console.log('Hello World')
  }
}
@Inject()
class AppController {
  constructor (private readonly appService: AppService) {}
  print () {
    this.appService.sayHello()
  }
}
function Factory<T>(target: Contructor<T>): T {
  const providers = Reflect.getMetadata('design:paramtypes', target)
  const args = providers.map((provider: Contructor) => {
    return new provider()
  })
  return new target(...args)
}
Factory(AppController).print()
  • 第三步: 关上冰箱门, 运行代码; 完美的输出了Hello World

控制器

在 Nest.js 中 控制器 是用来 处理 和 响应请求的, 也就是 requestresponse;

控制器的目的是接收应用的特定请求。路由机制控制哪个控制器接收哪些请求。通常,每个控制器有多个路由,不同的路由可以执行不同的操作。

为了创建一个基本的控制器,我们使用类和装饰器。装饰器将类与所需的元数据相关联,并使 Nest 能够创建路由映射(将请求绑定到相应的控制器)。

使用TypeScript的装饰器实现简易Nest.js

import { Controller, Get, Post } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get('ab*cd') // 处理 get 请求
  getHello(): string {
    return this.appService.getHello();
  }

  @Post('post') // 处理 post请求
  postMethod(): string {
    return 'Hello World';
  }
}

简易代码实现

import 'reflect-metadata'

const MEHTOD_METADATA = 'method'
const PATH_MEDADATA = 'path'
/**
 * Controller 装饰器
 * @param path
 * @returns
 */
function Controller(path: string): ClassDecorator {
  return function (target) {
    Reflect.defineMetadata(PATH_MEDADATA, path, target)
  }
}

/**
 * Methods 装饰器
 * @param method
 * @returns
 */
function createMappingDecorator (method: string)  {
  return function (path: string): MethodDecorator {
    return function(target: Object, key: string | symbol, descriptor: PropertyDescriptor) {
      Reflect.defineMetadata(MEHTOD_METADATA, method, descriptor.value)
      Reflect.defineMetadata(PATH_MEDADATA, path, descriptor.value)
    }
  } 
}

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

function isFunction(fn: unknown) {
  return typeof fn === 'function'
}

function mapRoute (instance: Object) {
  const prototype = Object.getPrototypeOf(instance)
  const methodsNames = Object.getOwnPropertyNames(prototype).filter((item: string) => {
    return !isConstructor(item) && isFunction(prototype[item])
  })

  return methodsNames.map((name: string) => {
    const fn: Function = prototype[name]
    const route: string = Reflect.getMetadata(PATH_MEDADATA, fn)
    const method: string = Reflect.getMetadata(MEHTOD_METADATA, fn)

    return {
      route,
      method,
      fn,
      name
    }
  })
}
const Get = createMappingDecorator('Get')
const Post = createMappingDecorator('Post')

@Controller('/test')
class SomeClass {
  @Get('/a')
  someMethod (): string {
    return 'Hello World'
  }

  @Post('/b')
  somePostMethod (): string {
    return 'Post'
  }
}

const routes = mapRoute(new SomeClass)
console.log(routes)

这样我们就可以拿到一个 路由对象

使用TypeScript的装饰器实现简易Nest.js

这样的话,我们把处理过的路由对象, 交给 express 处理 代码如下:

// decrotor.ts
import 'reflect-metadata'

const MEHTOD_METADATA = 'method'
const PATH_MEDADATA = 'path'
/**
 * Controller 装饰器
 * @param path
 * @returns
 */
export function Controller(path: string): ClassDecorator {
  return function (target) {
    Reflect.defineMetadata(PATH_MEDADATA, path, target)
  }
}

/**
 * Methods 装饰器
 * @param method
 * @returns
 */
function createMappingDecorator (method: string)  {
  return function (path: string): MethodDecorator {
    return function(target: Object, key: string | symbol, descriptor: PropertyDescriptor) {
      Reflect.defineMetadata(MEHTOD_METADATA, method, descriptor.value)
      Reflect.defineMetadata(PATH_MEDADATA, path, descriptor.value)
    }
  } 
}

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

function isFunction(fn: unknown) {
  return typeof fn === 'function'
}

export function mapRoute (instance: Object) {
  const prototype = Object.getPrototypeOf(instance)
  const methodsNames = Object.getOwnPropertyNames(prototype).filter((item: string) => {
    return !isConstructor(item) && isFunction(prototype[item])
  })
  const parentPath = Reflect.getMetadata(PATH_MEDADATA, prototype.constructor)
  const routes = methodsNames.map((name: string) => {
    const fn: Function = prototype[name]
    const route: string = Reflect.getMetadata(PATH_MEDADATA, fn)
    const method: string = Reflect.getMetadata(MEHTOD_METADATA, fn).toLowerCase()

    return {
      route,
      method,
      fn,
      name
    }
  })
  return {
    root: parentPath,
    routes
  }
}
export const Get = createMappingDecorator('Get')
export const Post = createMappingDecorator('Post')
// index.ts
import { Controller, Get, Post, mapRoute } from './decrotor'
import express, { Request, Response, Router} from 'express'

interface Route {
  route: string;
  method: string;
  fn: () => void;
  name: string
}

interface CustomRouter {
  root: string,
  routes: Route[]
}



@Controller('/test')
class SomeClass {
  @Get('/a')
  someMethod (): string {
    return 'Get Hello World'
  }

  @Post('/b')
  somePostMethod (): string {
    return 'Post'
  }
}
const app = express()
const retRouter: CustomRouter = mapRoute(new SomeClass) as CustomRouter
const router: Router = express.Router()

retRouter.routes.forEach((route: Route) => {
  if (route.method === 'get') {
    router.get(route.route, (req: Request, res: Response) => {
      const value = route.fn()
      res.send(value)
    })
    return
  }
  if (route.method === 'post') {
    router.post(route.route, (req: Request, res: Response) => {
      const value = route.fn()
      res.send(value)
    })
  }
})

app.get('/', function (req: Request, res: Response) {
  res.send('Home Hello World')
})

app.use(retRouter.root, router)
app.listen(3000)

使用TypeScript的装饰器实现简易Nest.js 这样我们就简单的实现了一个简易的Nest.js