ExpressをつかってAPIサーバを作っていたけど、
Spring Bootみたいに、デコレータでマッピングしたいなと思い、
いろいろ調べてみたときの備忘録。
※ routing-controllersというライブラリもあるけど、デコレータ理解のためreflect-metadataを使ってます。
やりたいこと
こんな感じで、クラスのメソッドにデコレータをつけて、
パスを設定したい感じ。
// ./AuthController.ts import { Request, Response } from "express"; import { Controller, Get, Post, SimpleController } from "./decorators"; @Controller("/auth") class AuthController extends SimpleController { @Post("/login") public async login(req: Request, res: Response) { const data = "..." return res.json(data).end(); } @Get("/me") public async me(req: Request, res: Response) { const data = "..." return res.json(data).end(); } } export const authController = new AuthController();
あと、route()
を生やして、
クラスにある全ルートを取得できるようにしたい。
// ./routers.ts import { Router } from "express"; import { authController } from "./AuthController"; const _router = Router(); _router.use(authController.route()); export const router = _router;
やったこと
全体としてはこんな感じ。
SimpleController
- @Controlerをつけるクラスの基底クラス
route()
でrouterを取得できるようにするために用意
@Controller
- クラスにつけるデコレータ
- コンストラクタで
@Get
などがついてるメソッドを収集・設定
@Get
、@Post
、@Patch
、@Delete
- メソッドにつけるデコレータ
- 設定しているURLパスとマッピングする
デコレータの取得には、reflect-metadata
を使っていて、
以下のような感じで使っている。
- メソッドのデコレータ(
@Get
など)Reflect.defineMetadata()
でメタデータを設定
- クラスのデコレータ(
@Controller
)Reflect.getMetadata()
でメタデータを取得
// ./decorators.ts import { Request, Response, Router } from "express"; import "reflect-metadata"; // @Controlerをつけるクラスの基底クラス // route()を追加している export abstract class SimpleController { baseRoute!: Router; path!: string; route() { const router = Router(); router.use(this.path, this.baseRoute); return router; } } // reflect-metadataで利用する型定義 // Methodの種類や設定するメタデータの方を用意 type Method = "get" | "post" | "patch" | "delete"; interface RouteMetadata { path: string; method: Method; actionName: string; } const ACTION_KEY = Symbol("action"); // @Controlerの定義 // constructorを拡張して、メソッドからrouterを作成 export function Controller(path: string) { return (fn: new () => SimpleController) => class extends fn { constructor() { super(); // routerの作成 const route = Router(); // reflect-metadataを使って、アノテーション(メタデータ)を取得 const list: RouteMetadata[] = Reflect.getMetadata( ACTION_KEY, fn.prototype ); // アノテーション(メタデータ)ごとに対象のメソッドをラップして、 // routerにパスを設定 list.forEach((meta) => { const wrapFunc = async (req: Request, res: Response) => { const ret = await (this as any)[meta.actionName](req, res); return ret; }; route[meta.method](meta.path, wrapFunc); }); // 設定が終わったrouterと@Controlerのpathを保存 this.baseRoute = route; this.path = path; } } as any; } // 各HTTPメソッドのデコレータの定義(@Get/@Postなど) export const Get = mappingFactory("get"); export const Post = mappingFactory("post"); export const Patch = mappingFactory("patch"); export const Delete = mappingFactory("delete"); // HTTPメソッドを共通化した関数 function mappingFactory(method: Method) { return (path: string = "/") => (target: any, actionName: string, dsc: PropertyDescriptor) => { // デコレータがついているメソッドに対する処理 // reflect-metadataを使って、アノテーションのメタデータを設定 const meta: RouteMetadata = { path, method, actionName }; addMetadata(meta, target, ACTION_KEY); }; } // アノテーションのメタデータを設定する処理 function addMetadata(metadataValue: RouteMetadata, target: any, metadataKey: Symbol) { const list = Reflect.getMetadata(metadataKey, target); if (list) { list.push(metadataValue); return; } Reflect.defineMetadata(metadataKey, [metadataValue], target); }
クラスのデコレータ
クラスのデコレータは、クラスを返す関数になっている。
受け取ったconstructor
を継承して、オーバーライドすることで、
任意の処理などを加えることができる。
// @reportableClassDecoratorデコレータ function reportableClassDecorator<T extends { new (...args: any[]): {} }>(constructor: T) { return class extends constructor { reportingURL = "http://www..."; }; } @reportableClassDecorator class BugReport { type = "report"; title: string; constructor(t: string) { this.title = t; } }
・参考: Class Decorators
メソッドのデコレータ
メソッドのデコレータは、特定の引数をもつ関数を返す関数になっている。
target
... メソッドを定義しているクラスpropertyKey
... メソッド名descriptor
... メソッドの情報
// @enumerableデコレータ function enumerable(value: boolean) { return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { descriptor.enumerable = value; }; } class Greeter { greeting: string; constructor(message: string) { this.greeting = message; } @enumerable(false) greet() { return "Hello, " + this.greeting; } }
デコレータ内の処理は、呼び出し時に行われるわけでな無いので注意。
AOPのようにメソッドの前後に処理をはさみたい場合は、
descriptor.value
を書き換える必要がある。
function addlog(target: any, prop: string, desc: PropertyDescriptor) { const original = desc.value; desc.value = function () { const key = `${target.constructor.name}#${prop}`; console.log(`START: ${key}`); const ret = Reflect.apply(original, this, arguments); console.log(`END: ${key}`); return ret; }; } class Greeter { @addlog greet() { return "Hello"; } } // => // START: Greeter#greet // Hello // END: Greeter#greet
デコレータの呼び出し順序
メソッドにデコレータを複数つけた場合、以下のような感じになる。
function f() { console.log("f(): evaluated"); return function (target: any, prop: string, desc: PropertyDescriptor) { console.log("f(): called"); }; } function g() { console.log("g(): evaluated"); return function (target: any, prop: string, desc: PropertyDescriptor) { console.log("g(): called"); }; } class C { @f() @g() foo() { console.log("foo"); } } new C().foo(); // => // f(): evaluated // g(): evaluated // g(): called // f(): called // foo
以上!! なかなか難しいけど、いろいろできそう(´ω`)