eggjs 装饰器路由|项目复盘

1,235 阅读3分钟

介绍

eggjs 是一个基于 node 的企业级 web 服务框架

egg-decorator-router 是基于 eggjs 开发的扩展插件,实现通过装饰器生成路由以及给每个路由单独定义中间件行为

动机

eggjs 通过 router 和 controller 实现 http 请求,定义方法如下

先定义 controller

// app/controller/user.js
class UserController extends Controller {
  async info() {
    const { ctx } = this;
    ctx.body = {
      name: `hello ${ctx.params.id}`,
    };
  }
}

之后定义 router

// app/router.js
module.exports = (app) => {
  const { router, controller } = app;
  router.get("/user/:id", controller.user.info);
};

这种定义方式需要先定义 controller 之后在统一的地方添加 router 和中间件,由实现可看出路由的定义和 controller 的定义是分开的,想加入一些非标准的 http 请求方法需要在两个地方改东西,理想的实现是直接在 controller 中用装饰器声明 http 请求方法,并且可以给每个方法单独定义中间件,类似一些后端技术中的实现模式

// app/controller/user.js

@Route("/user")
class UserController extends Controller {
  @HttpGet("/:id")
  @Middleware(async (ctx, next) => {
    // pre invoke
    await next();
    // post invoke
  })
  async info() {
    const { ctx } = this;
    ctx.body = {
      name: `hello ${ctx.params.id}`,
    };
  }
}

实现原理

想实现通过装饰器定义路由和中间件需要先了解一下装饰器的概念

js 中的 class 通常被称作是一种语法糖实现了 “类” 的定义

class UserController {
  info() {
    // 方法实现
  }
}

实际上 class 的本质是如下定义

function UserController() {}

Object.defineProperty(UserController.prototype, "info", {
  value: function () {},
  enumerable: false,
  configurable: true,
  writable: true,
});

装饰器是 ES6 语法特性,以装饰模式添加或改变 class 的成员,装饰器定义形式如下所示

function decoratorName(target, key, descriptor) {
  //
};

其中 target 是要装饰对象本身,key 是声明装饰的成员,descriptor 是成员对象的一些属性

eggjs 在应用启动之前会先加载 js 的定义,之后才会执行入口方法启动服务,于是可在装饰 controller 和 controller 的成员方法时传入 url 路径规则将请求和 controller 成员的映射关系存储起来,并在 app 启动时将存储的映射生成对应的路由,这样就能实现通过装饰器建立路由的功能

中间件可以在 router 中单独给 controller 的方法定义,因此只需要创建中间件的装饰器,将中间件也按照一定顺序存储到映射关系里

一般情况下单独定义一个请求的中间件

router.post("/user", isLoginUser, hasAdminPermission, controller.user.create);

使用装饰器定义请求的中间件

@Route("/user")
class UserController {
  @HttpPost()
  @Middleware(isLoginUser)
  @Middleware(hasAdminPermission)
  create() {
    // 方法实现
  }
}

使用说明

添加 egg-decorator-router

npm i egg-decorator-router

Http 请求的完整路径是根路径和子路径合并的结果,在 controller 中先引入依赖

const {
  Route,
  HttpAll,
  HttpGet,
  HttpPost,
  HttpPut,
  HttpPatch,
  HttpDelete,
  Middleware,
} = require("egg-decorator-router");

如果使用 typescript

import {
  Route,
  HttpAll,
  HttpGet,
  HttpPost,
  HttpPut,
  HttpPatch,
  HttpDelete,
  Middleware,
} from "egg-decorator-router";

在 controller 里定义一个根路径

// root path is '/'
@Route()

// root path is '/'
@Route('/')

// root path is '/routename'
@Route('/routename')

// root path is '/routename/action'
@Route('/routename/action')

支持定义参数

@Route('/routename/:name')

在 controller 方法上定义子 HttpMethod 声明路径,支持 Http 方法包括 HttpGet HttpPost HttpPut HttpPatch HttpDelete HttpAll

// sub-path is '/'
@HttpGet()

// sub-path is '/'
@HttpGet('/')

// sub-path is '/action'
@HttpGet('/action')

// sub-path is '/action/:id'
@HttpGet('/action/:id')

定义中间件,第一个参数是中间件实现,第二个参数是中间建排序,如不定义排序默认执行的是声明的顺序

@Middleware(routeM, 1)

示例

"use strict";

const { Controller } = require("egg");
const { Route, HttpGet, Middleware, filters } = require("egg-decorator-router");
const { DefaultFilter } = filters;

const routeM = (ctx, next) => {
  console.log("passed route middleware");
  next();
};

const actionM = (i) => {
  return (ctx, next) => {
    console.log("passed action middleware " + i);
    next();
  };
};

@Route()
@Middleware(routeM)
class HomeController extends Controller {
  @HttpGet("/") // path: /
  async index() {
    await new Promise((resolve) => {
      this.ctx.body = "ssss";
      resolve();
    });
  }

  @HttpGet() // path: /func1
  @Middleware(actionM(2), 2)
  @Middleware(actionM(1), 1)
  func1(ctx) {
    ctx.body = "hi, func1";
  }
}

module.exports = HomeController;

相关链接

项目实现

GitHub

Gitee


本文正在参与「掘金 2021 春招闯关活动」, 点击查看 活动详情