美文网首页让前端飞Web前端之路
TS装饰器初体验,用装饰器管理koa接口

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

作者: 超人鸭 | 来源:发表于2020-04-20 19:09 被阅读0次

    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,如果你有更好的看法见解,欢迎指教哦

    作者微信:Aqing1906

    相关文章

      网友评论

        本文标题:TS装饰器初体验,用装饰器管理koa接口

        本文链接:https://www.haomeiwen.com/subject/yubdihtx.html