基於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框架,開發體驗很是好
相關文章
相關標籤/搜索