资讯专栏INFORMATION COLUMN

黄金搭档 -- JS 装饰器(Decorator)与Node.js路由

simon_chen / 1314人阅读

摘要:即为装饰器函数的这里主要为了获取路由路径的前缀,为请求方法,为请求路径,为请求执行的函数。下边是设置路由路径前缀和塞入内容的装饰器函数就不多说了,就是挂载前缀路径到类的原型对象上,这里需要注意的是作用于类,所以是被修饰的类本身。

很多面对象语言中都有装饰器(Decorator)函数的概念,Javascript语言的ES7标准中也提及了Decorator,个人认为装饰器是和async/await一样让人兴奋的的变化。正如其“装饰器”的叫法所表达的,他可以对一些对象进行装饰包装然后返回一个被包装过的对象,可以装饰的对象包括:类,属性,方法等。
Node.js目前已经支持了async/await语法,但decorator还需要babel的插件支持,具体的配置不在叙述。(截至发稿时间2018-12-29)
下面是引用的关于decorator语法的一个示例:

@testable
class Person {
  @readonly
  @nonenumerable
  name() { return `${this.first} ${this.last}` }
}

从上面代码中,我们一眼就能看出,Person类是可测试的,而name方法是只读和不可枚举的。

关于 Decorator 的详细介绍参见下面两篇文章:

阮一峰《ECMAScript 6 入门》 -- Decorator

知乎 -- 《Decorators in ES7》

期望效果

关于Node.js中的路由,大家应该都很熟悉了,无论是在自己写的http/https服务中,还是在ExpressKoa等框架中。我们要为路由提供请求的URL和其他需要的GETPOST等参数,随后路由需要根据这些数据来执行相应的代码。
关于Decorator和路由的结合我们这次希望写出类似下面的代码:

@Controller("/tags")
export default class TagRouter {
  @Get(":/id")
  @Login
  @admin(["developer", "adminWebsite"])
  @require(["phone", "password"])
  @Log
  async getTagDetail(ctx, next) {
    //...
  }
}
关于这段代码的解释:
第一行,通过Controller装饰TagRouter类,为类下的路由函数添加统一路径前缀/tags
第二行,创建并导出TagRouter类。
第三行,通过装饰器为getTagDetail方法添加路径和请求方法。
第四行,通过装饰器限制发起请求需要用户登录。
第五行,通过装饰器限制发起请求的用户必须拥有开发者或者网站管理员权限。
第六行,通过装饰器检查请求参数必须包含phonepassword字段。
第七行,通过装饰器为请求打印log。
第八行,路由真正执行的方法。

这样不仅简化、规范化了路由的写法,减少了代码的冗余和错误,还使代码含义一目了然,无需注释也能通俗易懂,便于维护、交接等事宜。

具体实现

下面就着手写一个关于movies的路由具体实例,示例采用koa2 + koa-router为基础组织代码。

文件路径:/server/routers/movies.js

import mongoose from "mongoose";

import { Controller, Get, Log } from "../decorator/router";
import { getAllMovies, getSingleMovie, getRelativeMovies } from "../service/movie";

@Controller("/movies")
export default class MovieRouter {
  @Get("/all")
  @Log
  async getMovieList(ctx, next) {
    const type = ctx.query.type;
    const year = ctx.query.year;

    const movies = await getAllMovies(type, year);

    ctx.body = {
      data: movies,
      success: true,
    };
  }

  @Get("/detail/:id")
  @Log
  async getMovieDetail(ctx, next) {
    const id = ctx.params.id;
    const movie = await getSingleMovie(id);
    const relativeMovies = await getRelativeMovies(movie);

    ctx.body = {
      data: {
        movie,
        relativeMovies,
      },
      success: true,
    }
  }
}

代码中Controller为路由添加统一前缀,Get指定请求方法和路径,Log打印日志,参考上面的预期示例。

关于mongodb以及获取数据的代码这里就不贴出了,毕竟只是示例而已,大家可以根据自己的资源,自行修改为自己的逻辑。

重点我们看一下,GET /movies/all以及GET /movies//detail/:id这两个路由的装饰器实现。

文件路径:/server/decorator/router.js

import KoaRouter from "koa-router";
import { resolve } from "path";
import glob from "glob"; // 使用shell模式匹配文件

export class Route {
  constructor(app, routesPath) {
    this.app = app;
    this.router = new KoaRouter();
    this.routesPath = routesPath;
  }

  init = () => {
    const {app, router, routesPath} = this;
    glob.sync(resolve(routesPath, "./*.js")).forEach(require);
    // 具体处理逻辑
    app.use(router.routes());
    app.use(router.allowedMethods());
  }
};

首先,导出一个Route类,提供给外部使用,Route类的构造函数接收两个参数approutesPathapp即为koa2实例,routesPath为路由文件路径,如上面movies.jsroutesPath/server/routers/

然后,提供一个初始化函数init,初始化逻辑中。引用所有routesPath下的路由,并use路由实例。

这样的话我们就可以在外部这样调用Route类:

import {Route} from "../decorator/router";
import {resolve} from "path";

export const router = (app) => {
  const routesPath = resolve(__dirname, "../routes");
  const instance = new Route(app, routesPath);

  instance.init();
}

好了,基本框架搭好了,来看具体逻辑的实现。

先补充完init方法:

文件路径:/server/decorator/router.js

  const pathPrefix = Symbol("pathPrefix");

  init = () => {
    const {app, router, routesPath} = this;
    glob.sync(resolve(routesPath, "./*.js")).forEach(require);

    R.forEach( // R为"ramda"方法库,类似"lodash"
      ({target, method, path, callback}) => {
        const prefix = resolvePath(target[pathPrefix]);
        router[method](prefix + path, ...callback);
      }
    )(routeMap)
    
    app.use(router.routes());
    app.use(router.allowedMethods());
  }

为了加载路由,需要一个路由列表routeMap,然后遍历routeMap,挂载路由,init工作就完成了。
下边的重点就是向routeMap中塞入数据,这里每个路由对象采用object的形式有四个key,分别为target, method, path, callback

target即为装饰器函数的target(这里主要为了获取路由路径的前缀),method为请求方法,path为请求路径,callback为请求执行的函数。

下边是设置路由路径前缀和塞入routeMap内容的装饰器函数:

export const Controller = path => (target, key, descriptor) => {
  target.prototype[pathPrefix] = path;
  return descriptor;
}

export const setRouter = method => path => (target, key, descriptor) => {
  routeMap.push({
    target,
    method,
    path: resolvePath(path),
    callback: changeToArr(target[key]),
  });
  return descriptor;
}

Controller就不多说了,就是挂载前缀路径到类的原型对象上,这里需要注意的是Controller作用于类,所以target是被修饰的类本身。

setRouter函数也很简单把接受到的路径格式化处理,把路由处理函数包装成数组,之后与targetmethod一起构造城对象塞入routeMap

这里有两个辅助函数,简单贴下代码看下:

import R from "ramda"; // 类似"lodash"的方法库

// 如果路径是以/开头直接返回,否则补充/后返回
const resolvePath = R.unless(
  R.startsWith("/"),
  R.curryN(2, R.concat)("/"),
);

// 如果参数是函数直接返回,否则包装成数组返回
const changeToArr = R.unless(
  R.is(Array),
  R.of,
);

接下来是getpostputdelete方法的具体实现,其实就是调用setRouter就行了:

export const Get = setRouter("get");

export const Post = setRouter("post");

export const Put = setRouter("put");

export const Delete = setRouter("delete");

至此,主要的功能就全部实现了,接下来是一些辅助Decorator,大家可以参考和使用core-decorators.js,它是一个第三方模块,提供了几个常见的修饰器,通过它也可以更好地理解修饰器。

下面以Log为示例,实现一个辅助Decorator,其他Decorator大家自己发挥:

let logTimes = 0;

export const convert = middleware => (target, key, descriptor) => {
  target[key] = R.compose(
    R.concat(
      changeToArr(middleware)
    ),
    changeToArr,
  )(target[key]);

  return descriptor;
}

export const Log = convert(async (ctx, next) => {
  logTimes++;
  console.time(`${logTimes}: ${ctx.method} - ${ctx.url}`);
  await next();
  console.timeEnd(`${logTimes}: ${ctx.method} - ${ctx.url}`);
})
convert是一个辅助函数,首先把普通函数转换成数组,然后跟其他中间件函数合并。此辅助函数也可用于其他辅助Decorator。

好了,到此文章就结束了,大家多交流,本人github
下一篇:分享koa2源码解读

最后贴出关键的/server/decorator/router.js的完整代码

import R from "ramda";
import KoaRouter from "koa-router";
import glob from "glob";
import {resolve} from "path";

const pathPrefix = Symbol("pathPrefix")
const routeMap = [];
let logTimes = 0;

const resolvePath = R.unless(
  R.startsWith("/"),
  R.curryN(2, R.concat)("/"),
);

const changeToArr = R.unless(
  R.is(Array),
  R.of,
);

export class Route {
  constructor(app, routesPath) {
    this.app = app;
    this.router = new KoaRouter();
    this.routesPath = routesPath;
  }

  init = () => {
    const {app, router, routesPath} = this;
    glob.sync(resolve(routesPath, "./*.js")).forEach(require);

    R.forEach(
      ({target, method, path, callback}) => {
        const prefix = resolvePath(target[pathPrefix]);
        router[method](prefix + path, ...callback);
      }
    )(routeMap)
    app.use(router.routes());
    app.use(router.allowedMethods());
  }
};

export const Controller = path => (target, key, descriptor) => {
  console.log(target);
  target.prototype[pathPrefix] = path;
  return descriptor;
}

export const setRouter = method => path => (target, key, descriptor) => {
  console.log("setRouter");
  routeMap.push({
    target,
    method,
    path: resolvePath(path),
    callback: changeToArr(target[key]),
  });
  return descriptor;
}

export const Get = setRouter("get");

export const Post = setRouter("post");

export const Put = setRouter("put");

export const Delete = setRouter("delete");

export const convert = middleware => (target, key, descriptor) => {
  target[key] = R.compose(
    R.concat(
      changeToArr(middleware)
    ),
    changeToArr,
  )(target[key]);

  return descriptor;
}

export const Log = convert(async (ctx, next) => {
  logTimes++;
  console.time(`${logTimes}: ${ctx.method} - ${ctx.url}`);
  await next();
  console.timeEnd(`${logTimes}: ${ctx.method} - ${ctx.url}`);
})

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/109545.html

相关文章

  • 美颜相机中的设计模式 —— 装饰者模式

    摘要:这是设计模式系列的第二篇,系列文章目录如下用一句话总结那些殊途同归的设计模式工厂策略模版方法美颜相机中的设计模式装饰者模式几乎所有的设计模式都是通过增加一层抽象来解决问题。 这是设计模式系列的第二篇,系列文章目录如下: 用一句话总结那些殊途同归的设计模式:工厂=?策略=?模版方法 美颜相机中的设计模式——装饰者模式 几乎所有的设计模式都是通过增加一层抽象来解决问题。 上一篇中提...

    anonymoussf 评论0 收藏0
  • 关于 express 路由管理的几种自动化方法

    摘要:在方法装饰器的编写上,由于装饰器的行为相似,因此我们可以编写一个抽象函数,用来生成不同请求方法的不同装饰器。文章博客地址关于路由管理的几种自动化方法 前言 我们平时在使用express写代码的过程中,会根据类别,将路由分为多个不同的文件,然后在项目的入口文件(例如app.js)中将其依次挂载,例如: const index = require(./routes/index) const...

    chaosx110 评论0 收藏0
  • Decorator控制Koa路由

    摘要:本文首发于用控制路由在中长这样还有上的框架两者都用来控制路由,这样写的好处是更简洁更优雅更清晰。反观或上的路由完全差了一个档次从开始就有了,只是浏览器和都还没有支持。 本文首发于:用Decorator控制Koa路由 showImg(https://segmentfault.com/img/remote/1460000015348698); 在Spring中Controller长这样 @...

    Hwg 评论0 收藏0
  • Python装饰-装饰流程,执行顺序

    摘要:最近看到一个关于的题文章其中的一个是装饰器的顺序问题就想写篇博客回顾下装饰器首先强烈推荐很久之前看的一篇博文翻译理解中的装饰器关于什么是装饰器看这篇文章就好了这里主要想写关于多个装饰器的执行流程装饰顺序示例代码初始化初始化输出结果初始化初始 最近看到一个关于Flask的CTF(RealWorld CTF 2018 web题bookhub)文章其中的一个trick是装饰器的顺序问题,就想...

    cpupro 评论0 收藏0
  • 在 Web 应用中使用 ES7 装饰Decorator)初体验

    摘要:前言今天闲来时看了看中的新标准之一,装饰器。过程中忽觉它和中的注解有一些类似之处,并且当前版本的中已经支持它了,所以,就动手在一个应用中尝鲜初体验了一番。另外,由于装饰器目前还是中的一个提案,其中具体细节可能还会更改。 前言 今天闲来时看了看ES7中的新标准之一,装饰器(Decorator)。过程中忽觉它和Java中的注解有一些类似之处,并且当前版本的TypeScript中已经支持它了...

    ivan_qhz 评论0 收藏0

发表评论

0条评论

simon_chen

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<