JavaScript世界的装饰器

156 阅读3分钟

什么是装饰器

装饰器是对类、函数、属性之类的一种装饰,可以针对其添加一些额外的行为。

通俗的理解可以认为就是在原有代码外层包装了一层处理逻辑。

装饰器(Decorator)用来增强 JavaScript 类(class)的功能,许多面向对象的语言都有这种语法,目前有一个提案将其引入了 ECMAScript。

装饰器是一种函数,写成@ + 函数名,可以用来装饰四种类型的值。

  • 类的属性
  • 类的方法
  • 属性存取器(accessor)

所以,对于装饰器,可以简单地理解为是非侵入式的行为修改。

为什么要用装饰器

可能有些时候,我们会对传入参数的类型判断、对返回值的排序、过滤,对函数添加节流、防抖或其他的功能性代码,基于多个类的继承,各种各样的与函数逻辑本身无关的、重复性的代码。

在Vue中使用装饰器

配置如下:

// babel.config.js
plugins: [
    ['@babel/plugin-proposal-decorators', { legacy: true }],
    ['@babel/plugin-proposal-class-properties', { loose: true }]
  ]
// jsconfig.json
"experimentalDecorators": true
// package.json
"parserOptions": {
  "ecmaFeatures": {
    "legacyDecorators": true
  }
}

// 场景一,在methods中使用防重复提交

// utils/decorator.js
/**
 * 节流,一定时间内,只能触发一次操作
 * @export
 * @param {Function} fn - 运行函数
 * @param {Number} wait - 延迟时间
 * @returns
 */
export function throttle(wait) {
  return function(target, name, descriptor) {
    const fn = descriptor.value
    let canRun = true
    descriptor.value = function() {
      const _this = this._isVue ? this : target
      if (!canRun) return
      fn.apply(_this, arguments)
      canRun = false
      setTimeout(() => {
        canRun = true
      }, wait)
    }
  }
}
// xx/templete.vue
import { throttle } from '@utils/decorator.js'
methods: {
    @throttle(500)
    handleSubmit() {
        console.log('试试就试试')
    }
}

// 场景二,自动开启 loading
export function autoSwitch(loadingKey = 'loading') {
  return function (target, name, descriptor) {
    const oldFn = descriptor.value
    descriptor.value = async function (...args) {
      try {
        this[loadingKey] = true
        await oldFn.apply(this, args)
      } catch (error) {
        errorCb.call(this, error, this)
      } finally {
        this[loadingKey] = false
      }
    }
  }
}
// xxx/templtet.vue
import { autoSwitch } from '@utils/decorator.js'
methods: {
    @autoSwitch('loading')
    async handleSubmit() {
        try {
          const submitData = {/*参数*/}
            const res = await this.$request('Post', submitData)
            console.log(res)
        } catch (error) {
            console.log(error)
        }
        
    }
}

装饰器是用在类class声明,方法,访问符,属性或参数上,而 reactivesetup 本身是一个 function,依据 阮一峰装饰器为什么不能用于函数,在Vue3中我们有更好的抽象逻辑处理方式:hooks

多个装饰器的应用

装饰器是可以同时应用多个的(不然也就失去了最初的意义)。

用法如下:

@decorator1
@decorator2
class { }
/*
执行的顺序为decorator2 -> decorator1,离class定义最近的先执行。
可以想像成函数嵌套的形式:
decorator1(decorator2(class {}))
*/

function dec(id){
  console.log('before', id);
  return (target, property, descriptor) => console.log('after', id);
}

@dec('class 1')
@dec('class 2')
class Example {
  @dec('method 1')
  @dec('method 2')
  method(){}
  @dec('methodB 1')
  methodB(){}
}

/*
//  输出结果
before class 1
before class 2
before method 1
before method 2
before methodB 1
after method 2
after method 1
after methodB 1
after class 2
after class 1
*/

会像剥洋葱一样,先从外到内进入,然后由内向外执行

  • 先进入class 再进入method ; 先离开method 再离开method
  • 先进入method 1 再进入method2 ;先离开method2 再离开method 1

探索 Node.js 框架中的装饰器

在写Node接口时,可能是用的koaexpressNest,一般来说可能要处理很多的请求参数,有来自headers的,有来自body的,甚至有来自querycookie的。

所以很有可能在router的开头数行都是这样的操作:

router.get('/', async (ctx, next) => {
  let id = ctx.query.id
  let uid = ctx.cookies.get('uid')
  let device = ctx.header['device']
})

以及如果我们有大量的接口,可能就会有大量的router.getrouter.post

以及如果要针对模块进行分类,可能还会有大量的new Router的操作。

这些代码都是与业务逻辑本身无关的,所以我们应该尽可能的简化这些代码的占比,而使用装饰器就能够帮助我们达到这个目的。

我们可以手动实现一个路由装饰器:

// 首先,我们要创建几个用来存储信息的全局List
export const routerList      = []
export const controllerList  = []
export const parseList       = []
export const paramList       = []

// 虽说我们要有一个能够创建Router实例的装饰器
// 但是并不会直接去创建,而是在装饰器执行的时候进行一次注册
export function Router(basename = '') {
 return (constrcutor) => {
    routerList.push({
      constrcutor,
      basename
    })
  }
}

// 然后我们在创建对应的Get Post请求监听的装饰器
// 同样的,我们并不打算去修改他的任何属性,只是为了获取函数的引用
export function Method(type) {
 return (path) => (target, name, descriptor) => {
    controllerList.push({
      target,
      type,
      path,
      method: name,
      controller: descriptor.value
    })
  }
}

// 接下来我们还需要用来格式化参数的装饰器
export function Parse(type) {
 return (target, name, index) => {
    parseList.push({
      target,
      type,
      method: name,
      index
    })
  }
}

// 以及最后我们要处理的各种参数的获取
export function Param(position) {
 return (key) => (target, name, index) => {
    paramList.push({
      target,
      key,
      position,
      method: name,
      index
    })
  }
}

export const Body   = Param('body')
export const Header = Param('header')
export const Cookie = Param('cookie')
export const Query  = Param('query')
export const Get    = Method('get')
export const Post   = Method('post')

上边是创建了所有需要用到的装饰器,但是也仅仅是把我们所需要的各种信息存了起来,而怎么利用这些装饰器则是下一步需要做的事情了:

const routers = []

// 遍历所有添加了装饰器的Class,并创建对应的Router对象
routerList.forEach(item => {
  let { basename, constrcutor } = item
  let router = new Router({
    prefix: basename
  })

  controllerList
    .filter(i => i.target === constrcutor.prototype)
    .forEach(controller => {
      router[controller.type](controller.path, async (ctx, next) => {
        let args = []
       // 获取当前函数对应的参数获取
        paramList
          .filter( param => param.target === constrcutor.prototype && param.method === controller.method )
          .map(param => {
            let { index, key } = param
           switch (param.position) {
             case 'body':    args[index] = ctx.request.body[key] break
             case 'header':  args[index] = ctx.headers[key]      break
             case 'cookie':  args[index] = ctx.cookies.get(key)  break
             case 'query':   args[index] = ctx.query[key]        break
            }
          })

       // 获取当前函数对应的参数格式化
        parseList
          .filter( parse => parse.target === constrcutor.prototype && parse.method === controller.method )
          .map(parse => {
            let { index } = parse
           switch (parse.type) {
             case 'number':  args[index] = Number(args[index])             break
             case 'string':  args[index] = String(args[index])             break
             case 'boolean': args[index] = String(args[index]) === 'true'  break
            }
          })

       // 调用实际的函数,处理业务逻辑
        let results = controller.controller(...args)

        ctx.body = results
      })
    })

  routers.push(router.routes())
})

const app = new Koa()

app.use(bodyParse())
app.use(compose(routers))

app.listen(3000, () => console.log('server run as http://127.0.0.1:3000'))

上边的代码就已经搭建出来了一个Koa的封装,以及包含了对各种装饰器的处理,接下来就是这些装饰器的实际应用了:

import { Router, Get, Query, Parse } from "../decorators"

@Router('')
export default class {
  @Get('/')
  index (@Parse('number') @Query('id') id: number) {
   return {
      code: 200,
      id,
      type: typeof id
    }
  }

  @Post('/detail')
  detail (
    @Parse('number') @Query('id') id: number, 
    @Parse('number') @Body('age') age: number
  ) {
   return {
      code: 200,
      age: age + 1
    }
  }
}

Nest.js中路由装饰器的实现,源码地址

import {
  RESPONSE_PASSTHROUGH_METADATA,
  ROUTE_ARGS_METADATA,
} from '../../constants';
import { RouteParamtypes } from '../../enums/route-paramtypes.enum';
import { PipeTransform } from '../../index';
import { Type } from '../../interfaces';
import { isNil, isString } from '../../utils/shared.utils';

/**
 * The `@Response()`/`@Res` parameter decorator options.
 */
export interface ResponseDecoratorOptions {
  /**
   * Determines whether the response will be sent manually within the route handler,
   * with the use of native response handling methods exposed by the platform-specific response object,
   * or if it should passthrough Nest response processing pipeline.
   *
   * @default false
   */
  passthrough: boolean;
}

export type ParamData = object | string | number;
export interface RouteParamMetadata {
  index: number;
  data?: ParamData;
}

export function assignMetadata<TParamtype = any, TArgs = any>(
  args: TArgs,
  paramtype: TParamtype,
  index: number,
  data?: ParamData,
  ...pipes: (Type<PipeTransform> | PipeTransform)[]
) {
  return {
    ...args,
    [`${paramtype}:${index}`]: {
      index,
      data,
      pipes,
    },
  };
}

function createRouteParamDecorator(paramtype: RouteParamtypes) {
  return (data?: ParamData): ParameterDecorator =>
    (target, key, index) => {
      const args =
        Reflect.getMetadata(ROUTE_ARGS_METADATA, target.constructor, key) || {};
      Reflect.defineMetadata(
        ROUTE_ARGS_METADATA,
        assignMetadata<RouteParamtypes, Record<number, RouteParamMetadata>>(
          args,
          paramtype,
          index,
          data,
        ),
        target.constructor,
        key,
      );
    };
}

const createPipesRouteParamDecorator =
  (paramtype: RouteParamtypes) =>
  (
    data?: any,
    ...pipes: (Type<PipeTransform> | PipeTransform)[]
  ): ParameterDecorator =>
  (target, key, index) => {
    const args =
      Reflect.getMetadata(ROUTE_ARGS_METADATA, target.constructor, key) || {};
    const hasParamData = isNil(data) || isString(data);
    const paramData = hasParamData ? data : undefined;
    const paramPipes = hasParamData ? pipes : [data, ...pipes];

    Reflect.defineMetadata(
      ROUTE_ARGS_METADATA,
      assignMetadata(args, paramtype, index, paramData, ...paramPipes),
      target.constructor,
      key,
    );
  };

/**
 * Route handler parameter decorator. Extracts the `Request`
 * object from the underlying platform and populates the decorated
 * parameter with the value of `Request`.
 *
 * Example: `logout(@Request() req)`
 *
 * @see [Request object](https://docs.nestjs.com/controllers#request-object)
 *
 * @publicApi
 */
export const Request: () => ParameterDecorator = createRouteParamDecorator(
  RouteParamtypes.REQUEST,
);

/**
 * Route handler parameter decorator. Extracts the `Response`
 * object from the underlying platform and populates the decorated
 * parameter with the value of `Response`.
 *
 * Example: `logout(@Response() res)`
 *
 * @publicApi
 */
export const Response: (
  options?: ResponseDecoratorOptions,
) => ParameterDecorator =
  (options?: ResponseDecoratorOptions) => (target, key, index) => {
    if (options?.passthrough) {
      Reflect.defineMetadata(
        RESPONSE_PASSTHROUGH_METADATA,
        options?.passthrough,
        target.constructor,
        key,
      );
    }
    return createRouteParamDecorator(RouteParamtypes.RESPONSE)()(
      target,
      key,
      index,
    );
  };

/**
 * Route handler parameter decorator. Extracts reference to the `Next` function
 * from the underlying platform and populates the decorated
 * parameter with the value of `Next`.
 *
 * @publicApi
 */
export const Next: () => ParameterDecorator = createRouteParamDecorator(
  RouteParamtypes.NEXT,
);

/**
 * Route handler parameter decorator. Extracts the `Ip` property
 * from the `req` object and populates the decorated
 * parameter with the value of `ip`.
 *
 * @see [Request object](https://docs.nestjs.com/controllers#request-object)
 *
 * @publicApi
 */
export const Ip: () => ParameterDecorator = createRouteParamDecorator(
  RouteParamtypes.IP,
);

/**
 * Route handler parameter decorator. Extracts the `Session` object
 * from the underlying platform and populates the decorated
 * parameter with the value of `Session`.
 *
 * @see [Request object](https://docs.nestjs.com/controllers#request-object)
 *
 * @publicApi
 */
export const Session: () => ParameterDecorator = createRouteParamDecorator(
  RouteParamtypes.SESSION,
);

export function Body(
  ...pipes: (Type<PipeTransform> | PipeTransform)[]
): ParameterDecorator;
-----------太长不贴-------------
export const Req = Request;
export const Res = Response;

这样开发带来的好处就是,让代码可读性变得更高,在函数中更专注的做自己应该做的事情。

而且装饰器本身如果名字起的足够好的好,也是在一定程度上可以当作文档注释来看待了(相对于Java中的注解)。

总结

合理利用装饰器可以极大的提高开发效率,对一些非逻辑相关的代码进行封装提炼能够帮助我们快速完成重复性的工作,节省时间。

但是滥用装饰器也会使代码本身逻辑变得扑朔迷离,如果确定一段代码不会在其他地方用到,或者一个函数的核心逻辑就是这些代码,那么就没有必要将它取出来作为一个装饰器来存在。