JavaScript 装饰器(类 Java 中的注解) | 青训营

219 阅读3分钟

用过 Java 的同学可能熟悉注解(Annotation)。在 Java 中可以通过反射获取注解内容,从而加强注解标注的代码。最常用的注解莫过于 @Override,它用于标注方法是否是重写方法。SpringBoot 的许多配置也从最初的 XML 转向注解。

JavaScript 也有类似 Java 注解的功能,不过在 JavaScript 中其称作装饰器( Decorator),目前该提案处于 Stage 3。如下图,该语法趋于完善。

image.png (TC39 提案交付流程)

提案中对装饰器的正式定义是:

Decorators are functions called on classes, class elements, or other JavaScript syntax forms during definition.

装饰器是在定义过程中,在类、类元素或其他 JavaScript 语法单位上调用的函数。

装饰器有啥用呢?所谓装饰器就是在原先的基础上增加一些功能。

来看官方给的一个例子,装饰器 @logged 装饰类 C 的方法 m。达到的效果就是当 m 被调用,自动输出日志。

function logged(value, { kind, name }) {
  if (kind === "method") {
    return function (...args) {
      console.log(`starting ${name} with arguments ${args.join(", ")}`);
      const ret = value.call(this, ...args);
      console.log(`ending ${name}`);
      return ret;
    };
  }
}

class C {
  @logged
  m(arg) {}
}

new C().m(1);
// starting m with arguments 1
// ending m

再来看一个真实的例子,下面的代码都来自项目 cnpmcore。cnpmcore 是使用 tegg 框架搭建的,其中使用了许多装饰器,熟悉 SpringBoot 的同学应该会对该项目感到亲切。

下面的例子是通过装饰器 @Middleware 给类 MiddlewareController 加上了三个中间件 AlwaysAuthTracingErrorHandler。此处达到的效果是在调用 MiddlewareController 及其子类的方法时(自调用不算),会依次经过 AlwaysAuthTracingErrorHandler

import { Middleware } from '@eggjs/tegg';
import { AlwaysAuth } from './AlwaysAuth';
import { ErrorHandler } from './ErrorHandler';
import { Tracing } from './Tracing';

@Middleware(AlwaysAuth)
@Middleware(Tracing)
@Middleware(ErrorHandler)
export abstract class MiddlewareController {}

(src:/app/port/middleware/index.ts

例如 AlwaysAuth 就用于判断用户是否有权限访问,若无直接拦截请求。

import { EggContext, Next } from '@eggjs/tegg';
import { UserRoleManager } from '../UserRoleManager';

export async function AlwaysAuth(ctx: EggContext, next: Next) {
  if (ctx.app.config.cnpmcore.alwaysAuth) {
    // ignore login request: `PUT /-/user/org.couchdb.user::username`
    const isLoginRequest = ctx.method === 'PUT' && ctx.path.startsWith('/-/user/org.couchdb.user:');
    if (!isLoginRequest) {
      const userRoleManager = await ctx.getEggObject(UserRoleManager);
      await userRoleManager.requiredAuthorizedUser(ctx, 'read');
    }
  }
  await next();
}

(src:/app/port/middleware/AlwaysAuth.ts

更多实战例子请移步 cnpmcore,比如通过注解定义持久层。

mport { Attribute, Model } from '@eggjs/tegg/orm';
import { DataTypes, Bone } from 'leoric';

@Model()
export class Change extends Bone {
  @Attribute(DataTypes.BIGINT, {
    primary: true,
    autoIncrement: true,
  })
  id: bigint;

  @Attribute(DataTypes.DATE, { name: 'gmt_create' })
  createdAt: Date;

  @Attribute(DataTypes.DATE, { name: 'gmt_modified' })
  updatedAt: Date;

  @Attribute(DataTypes.STRING(24), {
    unique: true,
  })
  changeId: string;

  @Attribute(DataTypes.STRING(50))
  type: string;

  @Attribute(DataTypes.STRING(214))
  targetName: string;

  @Attribute(DataTypes.JSONB)
  data: any;
}

(src:/app/repository/model/Change.ts

总结,无论是 Java 中的注解,还是 JavaScript 中的装饰器都属于元编程的范畴。它的强大之处在于一个装饰器就可以引入许多重复、复杂的逻辑,比如 SpringBoot 中的注解已经将编程难度极大地降低。但不好之处在于元编程的代码可读性对初学者不太友好,有时很难看出某个装饰器到底干了啥。

在可预见的将来,JavaScript 语法会变得更强大。对 Java 程序员或许更友好,出现一个框架让他们可以像写 SpringBoot 一样用 JavaScript / TypeScript 编写后端代码(tegg 似乎是一个选择)。