TS装饰器初体验,用装饰器管理koa接口

typescript中的装饰器有很多种,比如类装饰器、方法装饰器、属性装饰器等等,先看看装饰器的定义吧,下面以类装饰器和方法装饰器为例子,顺便说几个点

类装饰器
// 装饰器都是以@符加在类或方法上面
@tsetDecorator
class Test {
  name = '超人鸭'
}
// 装饰器都是函数
function tsetDecorator(target: any) {
  console.log('装饰器')
}

现在我是没有创建这个类的实例的,只是声明,当我执行这个文件时就会打印出'装饰器'
从这个最简单的例子说几个点

  1. 类的装饰器有一个参数,为类的构造函数,通过这个参数可以改变类上的属性方法等
  2. 类的装饰器会在类定义后执行,不需要类实例化

再看一个简单的例子:

@tsetDecorator
class Test {
}

function tsetDecorator(target: any) {
  target.prototype.name = '吴彦祖'
}

console.log(Test.prototype) // {name:'吴彦祖'}
装饰器的工厂模式

装饰器是不可以直接传递参数的,但是之前看到vue里面用到装饰器的时候,好像都是可以传递参数的,比如prop装饰器:

@Prop(Boolean)
private visible: boolean | undefined

其实都是用到工厂模式,以上面设置name的例子,改写一下:

@tsetDecorator('吴彦祖')
class Test {
}

function tsetDecorator(name: string) {
  return function (target: any) {
    target.prototype.name = name
  }
}

console.log(Test.prototype) // {name:'吴彦祖'}
方法装饰器

直接上例子:

class Test {
  name = '超人鸭'

  @tsetDecorator
  getName() {
    return this.name
  }
}

function tsetDecorator(target: any, key: string, descriptor: PropertyDescriptor) {
  descriptor.value = function () {
    return '吴彦祖'
  }
  descriptor.writable = false
}

const test = new Test()
console.log(test.getName()) // 吴彦祖

test.getName = () => { // 报错
  return '111'
}

结合上面的例子讲一下方法装饰器涉及的点

  1. 和类装饰器一样,在类定义时方法装饰器就会执行
  2. 方法装饰器的三个参数,普通方法装饰器的第一个参数为类的prototype,静态方法装饰器的第一个参数为类的构造函数;第二个参数为方法的名称;第三个参数类似于object.defineproperty,可以对方法自身作某些修改,像上面例子一样,可以改变方法是否可改,以及方法本身。
  3. 当一个类有类装饰器和方法装饰器同时存在,执行的顺序是先执行方法装饰器再执行类装饰器

上面所说的就是装饰器一些基本的概念,下面再介绍一个库,通过这个库配合装饰器来管理koa的接口

reflect-metadata

这个库可以往类或方法上添加元数据,这个数据简单来说就是存在的,但是直接拿是拿不到的,得通过它的api存数据与取数据,先别懵逼,看到最后配合装饰器的使用就知道这个库的用处了。

import 'reflect-metadata'

class Test {
  getName() {
  }
}

// 存数据
Reflect.defineMetadata('data', 'test', Test)
Reflect.defineMetadata('data', 'test', Test, 'getName')

// 取数据
console.log(Reflect.getMetadata('data', Test)) // test
console.log(Reflect.getMetadata('data', Test, 'getName')) // test
  1. 用这个库直接import就可以了
  2. 存数据,通过Reflect.defineMetadata存储,第一个参数为存的数据的key,第二个参数为数据的值,第三个参数为存到哪个对象上(类),如果是存在方法上,那就有第四个参数,为存放的方法的方法名称。
  3. 取数据,通过Reflect.getMetadata取数据,第一个参数为数据的key,第二个参数为存放的对象(类),如果是存在方法上,那第三个参数为存放的方法的方法名称。

这就是reflect-metadata的基础使用方法,接下来结合装饰器对koa接口进行管理,先看看普通写koa接口的写法:

import Router from 'koa-router'
const router = new Router()

router.get('/', async (ctx) => {
  ctx.body = '访问根路径'
})

router.get('/testGet', async (ctx) => {
  ctx.body = '这个是get请求'
})

router.post('/testPost', async (ctx) => {
  ctx.body = '这个是post请求'
})

export default router

正常写koa接口时,都是按模块分类然后加上路由前缀,比如用户模块、订单模块等,像我上面的例子,三个接口当成是一个模块,那我们就可以把一个模块写成一个类来管理,先看改写后的类:

import Application from 'koa' // koa-router中的一个类型

@controller
class TestController{
  @get('/')
  async home(ctx: Application.ParameterizedContext<any, Router.IRouterParamContext<any, {}>>) {
    ctx.body =
      `
        <html>
          <body>
            <form method="post" action="/testPost">
              <button>post请求</button>
            </form>
            <form method="get" action="/testGet">
              <button>get请求</button>
            </form>
          </body>
        </html>
      `
  }
  
 @post('/testPost')
  async testPost(ctx: Application.ParameterizedContext<any, Router.IRouterParamContext<any, {}>>) {
    ctx.body = '这是post请求'
  }

  @get('/testGet')
  async testGet(ctx: Application.ParameterizedContext<any, Router.IRouterParamContext<any, {}>>) {
    ctx.body = '这是get请求'
  }
}

我把原来三个router接口改写成类中的三个方法,路径用装饰器进行传入,我的目的就是达到与普通写法一样,生成三个router接口,访问的路径与请求方法都一致,所以实现都放到装饰器里面,先看方法装饰器,也就是get和post方法:

function get(path: string){
  // 往方法上存上路径与请求方法
  return function (target: any, key: string) {
    Reflect.defineMetadata('path', path, target, key)
    Reflect.defineMetadata('method', 'get', target, key)
  }
}

function post(path: string){
  return function (target: any, key: string) {
    Reflect.defineMetadata('path', path, target, key)
    Reflect.defineMetadata('method', 'post', target, key)
  }
}
// 这两个方法都是高度相似的,可以再做一层封装,所以上面两个方法删掉,改成下面:

enum Methods { // 定义一个枚举类型
  get = 'get',
  post = 'post'
}

function getRequestDecorator(method: Methods) {
  return function (path: string) {
    return function (target: any, key: string) {
      Reflect.defineMetadata('path', path, target, key)
      Reflect.defineMetadata('method', method, target, key)
    }
  }
}

const get = getRequestDecorator(Methods.get)
const post = getRequestDecorator(Methods.post)

这样就给每个方法都绑定了对应的路径与请求方法,这里用到了枚举类型,用它来做类型校验,这种使用方式非常适合某个变量只能是固定的几个值得情况,像http请求,就是get、post、put等,不能是其他的。
到这里该绑定的都绑定了,接下来就是生成router的接口,我们在类的装饰器controller操作,因为方法执行器是先于类执行器,所以在类执行器里面可以操作到刚刚在方法装饰器中绑定的数据:

function controller(target: any) {
  for (let key in target.prototype) { // 遍历类上的方法
    const path: string = Reflect.getMetadata('path', target.prototype, key) // 拿到路径
    const method: Methods = Reflect.getMetadata('method', target.prototype, key) // 获取请求方法
    const hanlder = target.prototype[key] // 获取方法
    if (path && method && hanlder) {
      router[method](path, hanlder) // 注册router接口
    }
  }
}

到这里router接口就都注册完成了,下面是完整代码:

import Router from 'koa-router'
const router = new Router()
import Application from 'koa'
import 'reflect-metadata'

enum Methods {
  get = 'get',
  post = 'post'
}

function getRequestDecorator(method: Methods) {
  return function (path: string) {
    return function (target: any, key: string) {
      Reflect.defineMetadata('path', path, target, key)
      Reflect.defineMetadata('method', method, target, key)
    }
  }
}

const get = getRequestDecorator(Methods.get)
const post = getRequestDecorator(Methods.post)

function controller(target: any) {
  for (let key in target.prototype) {
    const path: string = Reflect.getMetadata('path', target.prototype, key)
    const method: Methods = Reflect.getMetadata('method', target.prototype, key)
    const hanlder = target.prototype[key]
    if (path && method && hanlder) {
      router[method](path, hanlder)
    }
  }
}

@controller
class TestController {
  @get('/')
  async home(ctx: Application.ParameterizedContext<any, Router.IRouterParamContext<any, {}>>) {
    ctx.body =
      `
        <html>
          <body>
            <form method="post" action="/testPost">
              <button>post请求</button>
            </form>
            <form method="get" action="/testGet">
              <button>get请求</button>
            </form>
          </body>
        </html>
      `
  }

  @post('/testPost')
  async testPost(ctx: Application.ParameterizedContext<any, Router.IRouterParamContext<any, {}>>) {
    ctx.body = '这是post请求'
  }

  @get('/testGet')
  async testGet(ctx: Application.ParameterizedContext<any, Router.IRouterParamContext<any, {}>>) {
    ctx.body = '这是get请求'
  }
}

export default router

接下来在koa的入口页,一般叫app.ts中引入这个router,下面是我的app.ts的全部代码

import Koa from 'koa'
const app = new Koa()
import Router from 'koa-router'
const router = new Router()

import testRouter from './controller/test' // 引入文件,class会自动定义
router.use('', testRouter.routes()) // 第一个参数为接口前缀,这里无前缀


app.use(router.routes())
app.use(router.allowedMethods())  // 允许http请求的所有方法

app.listen(3000, () => {
  console.log('服务开启在三千端口')
})

测试:


image.png

image.png

image.png

无问题,这就是我学了ts装饰器的小总结demo,如果你有更好的看法见解,欢迎指教哦

作者微信:Promise_fulfilled
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容