基于koa实现一个装饰器风格的框架

了解装饰器

装饰器(Decorator)是用来修改类行为的一个函数(语法糖),在许多面向对象语言中都有这个东西。node

语法

装饰器是一个函数,接受3个参数target name descriptorgit

  • target是被修饰的目标对象
  • name是被修饰的属性名
  • descriptor是属性的描述

定义一个装饰器函数github

function setName(target, name, descriptor) {
    target.prototype.name = 'hello world'
}

@setName
class A {
    
}
console.log((new A()).name) // hello world
复制代码

Edit TypeScript playground

差别

装饰器装饰不一样类型的目标是有一些差别的,这些差别体如今装饰函数接受的参数里面。typescript

Edit TypeScript playground

首先对一个类的装饰是由内到外的,先从类的属性开始,从上到下,按顺序修饰,若是类的属性是个方法,那么会先装饰这个方法的属性,再装饰这个方法。如上demo的consoleexpress

装饰Class

装饰函数接收到的参数 target是类的自己,namedescriptor都是undefinedapi

装饰Class的属性

装饰函数接收到的参数target是类的原型,也就是class.prototype数组

name为该属性的名字浏览器

当这个属性是个函数时:bash

descriptor为该方法的描述,经过Object.getOwnPropertyDescriptor(obj, prop)得到数据结构

当这个属性非函数时:

descriptorundefined

装饰Class方法的参数

装饰函数接受到的参数target是类的原型

name为该参数的名字

descriptor为该参数是这个函数的第几个参数,index:number

了解Reflect.metadate

Reflect能够理解为反射,能够改变Object的一些行为。

reflect.metadata从名字上看,就是对对象设置一些元数据。

有2个比较重要的api

Reflect.getMetadata(key, target)经过key得到在target上设置的元数据

Reflect.defineMetadata(key, value, target)经过key设置valuetarget

实现这个2个api不难,经过weakMapMap就能够实现了。

这样的数据结构

weakMap[target, Map[key, value]]

koa路由

koa的中间件模型不作介绍,koa-router就是个中间件。

路由其实就是映射一个controller方法到一个path字符串上。

经过ctxmatch匹配到的path而后调用这个controller方法。

简单的例子

在这个例子里面,经过装饰器,来实现绑定一个Controller方法到路由上。

首先如上所说的,有如下思路:

  1. 装饰器记录Controller元数据,实现一个Bind方法,取出元数据绑定到路由上

实现一个装饰器Router(path)用来装饰Controller的方法

import * as koa from "koa";
import * as router from "koa-router";

const koaRouter = new router();
const app = new koa();

function Router(path) {
  return function(target, name) {};
}

function bind(router, controller) {}
class Controller {
  @Router("/hello")
  sayHello(ctx) {
    ctx.body = "say hello";
  }
}
bind(koaRouter, Controller);
app.use(koaRouter.routes());
app.listen(8080);

复制代码

来实现bind方法和Router装饰器

首先是Router装饰器

function Router(path) {
  return function(target, name) {
    Reflect.defineMetadata("path", { path, name }, target);
  };
}
// 装饰器若是须要传参得再装饰器上层封装一个函数,而后再返回这个装饰器函数
复制代码

使用Reflect.metadata须要在程序的开始import "reflect-metadata";

首先是bind

function bind(router, controller) {
  const meta = Reflect.getMetadata("path", controller.prototype);
  console.log(meta);
  const instance = new controller();
  router.get(meta.path, ctx => {
    instance[meta.name](ctx);
  });
}
复制代码

这里的bind也很简单,首先是,装饰器装饰一个方法的target是类的原型,因此这边getMetadatatarget应该是controller.prototype,meta的属性path对应的是/hello name对应的是sayHello,而后就是实例化controller,而后经过router去绑定这个path和方法。

Edit node typescript

打开例子在右边的浏览器输入/hello就能看到say hello的输出。

进入正题

进入正题,开始封装一个不是那么完整的装饰器框架。

先定义一堆的constants

export enum METHODS {
    GET = 'get',
    POST = 'post',
    PUT = 'put',
    DEL = 'del',
    ALL = 'all'
}

export const PATH = 'DEC_PATH'
export const PARAM = 'DEC_PARAM'
复制代码

请求方法

首先是各类请求方法GET POST PUT DELETE

由于如今有了请求方法的区分,因此在收集信息的时候须要加一个字段。

如今收集信息的方法变为

import { METHODS, PATH } from "./constants";

export function Route(path: string, verb: METHODS) {
    return function(target, name, descriptor) {
        const meta = Reflect.getMetadata(PATH, target) || []
        meta.push({
            name,
            verb,
            path
        })
        Reflect.defineMetadata(PATH, meta, target)
    }
}
复制代码

能够看见,多了一个verb参数表示该controller的请求方法

这边用数组是由于,target只有这个controller要记录的信息不止一个有不少。

经过这个基础方法,再封装一下其余装饰器

export function ALL(path: string) {
    return Route(path, METHODS.ALL)
}

export function GET(path: string) {
    return Route(path, METHODS.GET)
}

export function POST(path: string) {
    return Route(path, METHODS.POST)
}

export function PUT(path: string) {
    return Route(path, METHODS.PUT)
}

export function DEL(path: string) {
    return Route(path, METHODS.DEL)
}
复制代码

装饰器写完,这里的bind应该和以前的不同,毕竟metadata是个数组,处理起来其实没有区别,加个循环罢了。

import * as Router from 'koa-router'
import * as Koa from 'koa'
import { PATH } from './constants';
 export function BindRoutes(koaRouter: Router, controllers: any[]) {
     for(const ctrl of controllers) {
         const pathMeta = Reflect.getMetadata(PATH, ctrl.prototype) || []
         console.log(pathMeta)
         const instance = new ctrl()

         for(const item of pathMeta) {
             const { path, verb, name } = item
             koaRouter[verb](path, (ctx: Koa.Context) => {
                instance[name](ctx)
             })
         }
     }
 }
复制代码

这里的pathMeta的输出:

[ { name: 'sayHello', verb: 'get', path: '/hello' },
  { name: 'postMessage', verb: 'post', path: '/post' },
  { name: 'putName', verb: 'put', path: '/put' },
  { name: 'delMe', verb: 'del', path: '/del' } ]
复制代码

Edit node typescript

点开例子右边的浏览输入/get就能预览获得,控制台也打印出来上面的输出。

请求参数

请求方法处理完了,处理一下请求参数。

举个例子

getUser(@Body() user, @Param('id') id) {
    
}
复制代码

想要的是,这个user参数自动变成ctx.body, id变为ctx.params.id

如上,绑定路由的时候,controller的参数是传进去的,而且,在装饰器对函数参数进行装饰的时候,能够经过descriptor得到到这个参数在全部参数里面的第几个位置。因此经过这些特性,能够实现想要的需求。

只要把bind方法改写成:

instance[name](arg1, arg2, arg3)
// arg1 = ctx.body
// arg2 = ctx.params.id
// arg3 = .....
复制代码

全部能从ctx中获取到的,均可以ctx.body ctx.params ctx.query

一样的,实现一个基础方法,叫作Inject来收集参数的信息

export function Inject(fn: Function) {
    return function(target, name, descriptor) {
        const meta = Reflect.getMetadata(PARAM, target) || []
        meta.push({
            name,
            fn,
            index: descriptor
        })
        Reflect.defineMetadata(PARAM, meta, target)
    }
}
复制代码

这里的的fn必须是个函数,由于须要经过请求的ctx拿到须要的值。这里的index是该变量在参数中的位置。

实现了Inject接下来继续实现其余的装饰器

export function Ctx() {
    return Inject(ctx => ctx)
}

export function Body() {
    return Inject(ctx => ctx.request.body)
}

export function Req() {
    return Inject(ctx => ctx.req)
}

export function Res() {
    return Inject(ctx => ctx.res)
}

export function Param(arg) {
    return Inject(ctx => ctx.params[arg])
}

export function Query(arg) {
    return Inject(ctx => ctx.query[arg])
}
复制代码

这些装饰器都很简单,都是基于Inject,这个装饰器的函数会先收集起来,后面会用到。

经过本身实现的bind函数能够很容易的把须要的参数传入到controller

看一下修改之后的bind函数

import * as Router from 'koa-router'
import * as Koa from 'koa'
import { PATH, PARAM } from './constants';
 export function BindRoutes(koaRouter: Router, controllers: any[]) {
     for(const ctrl of controllers) {
         const pathMeta = Reflect.getMetadata(PATH, ctrl.prototype) || []
         const argsMeta = Reflect.getMetadata(PARAM, ctrl.prototype) || []
         console.log(argsMeta)
         const instance = new ctrl()

         for(const item of pathMeta) {
             const { path, verb, name } = item
             koaRouter[verb](path, (ctx: Koa.Context) => {
                const args = argsMeta.filter(i => i.name === name).sort((a, b) => a.index - b.index).map(i => i.fn(ctx))
                instance[name](...args, ctx)
             })
         }
     }
 }
复制代码

argsfilter出这个controller方法有关的参数,再根据这些参数的index排序,排序之后就是args[i]的fn函数ctx => ctx.xxx的形式,经过执行fn(ctx)能够拿到须要的值。

最后执行controller的时候把这些值传入,就获得了想要的结果。

因此上面bind函数的args就是经过装饰器获得的所须要的参数。

这样来使用它们:

import { GET, PUT, DEL, POST, Ctx, Param, Body } from "../src";

export class Controller {
    @GET('/:id')
    sayHello (@Ctx() Ctx, @Param('id') id, @Query('name') name) {
        Ctx.body = 'hello' + id + name
    }

    @POST('/post')
    postMessage(@Body() body, ctx) {
        console.log(body)
        ctx.body = 'post'
    }
}
复制代码

当请求进入sayHello绑定的路由的时候, sayHello会被执行,而且会传入如下参数执行。

sayHello(ctx, ctx.params['id'], ctx.query['name'], ctx)

至此,就封装出了一个很简陋的装饰器风格的框架。

Edit node typescript

能够在右边的浏览地址输入123?name=w2fzu能够看到hello123w2fzu

总结

装饰器还能够作不少事情,在这里主要使用装饰器来记录一些信息,而后经过其余方法获取这些信息出来,进行处理。

装饰器风格的框架能够参考nestjs 这是一个彻底装饰器风格的框架,和Sprint boot很是像,能够尝试体验一下。

还有一些装饰器风格的库:

  1. Typeorm装饰器风格的ORM框架
  2. routing-controllers 装饰器风格的框架可使用expresskoa作底层
  3. trafficlight装饰器风格的框架,底层为koa
  4. nestjs一个很是棒的node框架,开发体验很是好
相关文章
相关标签/搜索