裝飾器(Decorator)是用來修改類行爲的一個函數(語法糖),在許多面向對象語言中都有這個東西。node
裝飾器是一個函數,接受3個參數target
name
descriptor
git
定義一個裝飾器函數github
function setName(target, name, descriptor) {
target.prototype.name = 'hello world'
}
@setName
class A {
}
console.log((new A()).name) // hello world
複製代碼
裝飾器裝飾不一樣類型的目標是有一些差別的,這些差別體如今裝飾函數接受的參數裏面。typescript
首先對一個類的裝飾是由內到外的,先從類的屬性開始,從上到下,按順序修飾,若是類的屬性是個方法,那麼會先裝飾這個方法的屬性,再裝飾這個方法。如上demo的consoleexpress
裝飾函數接收到的參數 target
是類的自己,name
與descriptor
都是undefined
api
裝飾函數接收到的參數target
是類的原型,也就是class.prototype
數組
name
爲該屬性的名字瀏覽器
當這個屬性是個函數時:bash
descriptor
爲該方法的描述,經過Object.getOwnPropertyDescriptor(obj, prop)
得到數據結構
當這個屬性非函數時:
descriptor
爲undefined
裝飾函數接受到的參數target
是類的原型
name
爲該參數的名字
descriptor
爲該參數是這個函數的第幾個參數,index:number
Reflect能夠理解爲反射,能夠改變Object
的一些行爲。
reflect.metadata從名字上看,就是對對象設置一些元數據。
有2個比較重要的api
Reflect.getMetadata(key, target)
經過key
得到在target
上設置的元數據
Reflect.defineMetadata(key, value, target)
經過key
設置value
到target
上
實現這個2個api不難,經過weakMap
和Map
就能夠實現了。
這樣的數據結構
weakMap[target, Map[key, value]]
koa的中間件模型不作介紹,koa-router
就是個中間件。
路由其實就是映射一個controller
方法到一個path
字符串上。
經過ctx
去match
匹配到的path
而後調用這個controller
方法。
在這個例子裏面,經過裝飾器,來實現綁定一個Controller
方法到路由上。
首先如上所說的,有如下思路:
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
是類的原型,因此這邊getMetadata
的target
應該是controller.prototype
,meta的屬性path
對應的是/hello
name
對應的是sayHello
,而後就是實例化controller
,而後經過router去綁定這個path
和方法。
打開例子在右邊的瀏覽器輸入/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' } ]
複製代碼
點開例子右邊的瀏覽輸入/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)
})
}
}
}
複製代碼
args
先filter
出這個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)
至此,就封裝出了一個很簡陋的裝飾器風格的框架。
能夠在右邊的瀏覽地址輸入123?name=w2fzu
能夠看到hello123w2fzu
裝飾器還能夠作不少事情,在這裏主要使用裝飾器來記錄一些信息,而後經過其餘方法獲取這些信息出來,進行處理。
裝飾器風格的框架能夠參考nestjs
這是一個徹底裝飾器風格的框架,和Sprint boot
很是像,能夠嘗試體驗一下。
還有一些裝飾器風格的庫:
ORM
框架express
和koa
作底層