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

栏目: Node.js · 发布时间: 6年前

内容简介:很多面对象语言中都有装饰器(Decorator)函数的概念,Javascript语言的ES7标准中也提及了Decorator,个人认为装饰器是和Node.js目前已经支持了下面是引用的关于

很多面对象语言中都有装饰器(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 的详细介绍参见下面两篇文章:

  1. 阮一峰《ECMAScript 6 入门》 -- Decorator
  2. 知乎 -- 《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 函数也很简单,把接受到的参数 path 做格式化处理,把 callback 函数包装成数组,之后与 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}`);
})

复制代码

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

价值再定义(腾讯金融产品体验设计之道)

价值再定义(腾讯金融产品体验设计之道)

腾讯FiT Design / / 电子工业 / 2018-08-01 / 81.0

一起来看看 《价值再定义(腾讯金融产品体验设计之道)》 这本书的介绍吧!

随机密码生成器
随机密码生成器

多种字符组合密码

MD5 加密
MD5 加密

MD5 加密工具

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试