使用koa2+es6/7打造高质量Restful API

前言

现在nodejs变得愈来愈火热,采用nodejs实现先后端分离架构已被多数大公司所采用。javascript

在过去,使用nodejs你们首先想到的是TJ大神写的express.js,而发展到现在,更轻量,性能更好的koa已然成为主流,html

它一样出自TJ大神手笔,现在版本已更新到了koa2,不只性能优异,它还支持async/await,堪称回调地狱的终结者前端

下面,咱们来探讨下,如何使用koa2+es6/7来打造高质量的Restful风格API。java

刨根问底,篇幅略长,精华在后面,须要耐心看。node

1. 两种模式

一种是耦合模式,即接口层和逻辑层都由一个函数来处理完成。git

另外一种是分离模式,即接口层和逻辑层是分开的。程序员

下面咱们先来讲第一种。es6

耦合模式

先举个粟子,以express为例:github

# /server/user/login.js   用户登陆

const express = require('express');
const router = express.Router();

 router.post('/api/user/login',function(req,res){

  // 逻辑层

 })

# /server/user/register.js  用户注册

const express = require('express');
const router = express.Router();

 router.post('/api/user/register',function(req,res){

  // 逻辑层

 })

# /server/user/put.js   更改用户资料

const express = require('express');
const router = express.Router();

 router.post('/api/user/put',function(req,res){

  // 逻辑层

 })

这种在过去很常见,相信不少人都写过,我也不例外。但并不推荐。web

首先,一个应用的api一般会不少,若是应用够复杂,那意味着你的api可能要处理很是多的逻辑。

而为了应付这些需求,你不得不创建不少的文件,甚至困扰于如何划分和组织好这些文件。

其次,后期并很差维护,当api过多,过于繁杂时,文件深层嵌套,也许你找一个api文件都费神费力。

分离模式

一样先来个粟子:

# /server/router.js

const express = require('express');
const router = express.Router();

 router.post('/api/user/login',require('../controllers/users/login'))         // 用户登陆

       .post('/api/user/register',require('../controllers/users/register'))   // 用户注册      

       .put('/api/user/put',require('../controllers/users/put')               // 更改用户资料

       .delete('/api/user/deluser',require('../controllers/users/deluser'))   // 删除用户
       ……

很显然,这种api已将接口层和逻辑层分离了,接口层由一个router.js文件来统必定义,而每一个接口的逻辑层则由单独的文件来处理,并按不一样功能模块用不一样文件夹来组织这些逻辑文件。

那么,这样作有什么好处呢?

首先,很直观,整个结构很清晰,一目了然

其次,只须要你专一于处理逻辑

再者,api集中在router.js文件定义,同事更容易看懂你的代码结构,或者知道你增改了哪些api等等,这很方便于多人协同开发,在大型开发中尤其重要

很显然,分离模式优于耦合模式。

2. 如何更好地组织逻辑层

通过上面的分析以后,咱们选择更优的分离模式, 它只须要你关注逻辑层。

可是,以上面分离模式的例子为例,每个接口仍然须要单独一个js文件来处理它的逻辑层,而且须要用不少不一样文件夹来组织它
们,假如应用足够大,有几十甚至上百个api,那意味着颇有可能你的js逻辑文件也达几十乃至上百个,而用来划分和组织这些js文
件的文件夹也不在少数。

这就形成了过于臃肿,难以维护的毛病。

那么,有没有可能,一个功能模块只须要一个js文件来处理它们的全部逻辑层,并更具可维护性呢?

打个比方,如今有一个博客站点,我仅使用一个user.js文件来处理用户模块全部api的逻辑层,包括注册,登陆,修改,删除,密码重置等等,另外用一个article.js文件来处理文章模块全部api的逻辑层,包括发布,修改,获取详情,点赞,评论,删除等等。

若是能够作到这样,那就意味着代码量大大减小,且可维护性更高。

而要作到这步,咱们须要解决两个问题,一个是异步回调,由于异步回调使咱们增长了不少代码量,逻辑复杂,二是如何批量定义和导出大量api的逻辑层方法。

首先,咱们先来解决异步回调这个问题,下面将会展开讲解。

为了减小篇幅,下面只作简要的浅析。

express 时代

咱们先来回顾一下历史。

鉴于nodejs的回调机制,不少异步操做都须要回调来完成,若是你的逻辑足够复杂,极可能就会陷进回调地狱,下面是一个简单的例子:

……
fs.readFile('/etc/password', function(err, data){
    // do something
    fs.readFile('xxxx', function(err, data){
        //do something
            fs.readFile('xxxxx', function(err, data){
            // do something
        })
    })
})
……

一样,express也不例外,经常会让你深陷回调地狱。一般一个api须要写大量的代码来完成,此时为了更好地开发和维护,你不得不每一个api都单独一个js文件来处理。

为了解决异步回调这个大问题,js生态出现了不少解决方案,

其中比较好的两个——promise,async。

promise, async时代

首先说说async

这曾是一个很是优秀的第三方模块,它基于回调机制来实现,是处理异步回调很好的解决方案,现在github上已超两万多颗星。

async提供两个很是好的处理异步的方法,分别是串行执行的waterfall,以及并行执行的parallel。

下面来个粟子:

# waterfall 按顺序执行,执行完一个,传递给下一个,最终结果返回给最后的回调函数

async.waterfall([
  function(callback){
    callback(null, 'one', 'two');
  },
  function(arg1, arg2, callback){
    // arg1 now equals 'one' and arg2 now equals 'two'
    callback(null, 'three');
  },
  function(arg1, callback){
    // arg1 now equals 'three'
    callback(null, 'done');
  }
], function (err, result) {
   // result now equals 'done'
   console.log(result);
});

# parallel 并行执行,即同时执行

async.parallel([
  function(callback){
    callback(null, 'one');
  },
  function(callback){
    callback(null, 'two');
  }
],
function(err, results){
  // 最终处理
});

很显然,这很大程度上避免了回调地狱,而且有一个完整的控制流,使你能够很好的组织代码。

接下来讲说promise

做为一名合格的前端,你有必要对promise有所了解,能够参考阮一峰写的es6入门之promise

首先,promise是es6的特性之一,实际是可用来传递异步操做流的对象。

promise提供三种状态,Pending(进行中),Resolved(已解决),Rejected(已失败)。

promise提供两个方法,resolve()和reject(),可用于处理最终结果。

promise还提供一个then方法,用于异步处理过程,这是一个控制流方法,能够不停地执行下去,直到获得你想要的结果。

promise还提供了catch方法,用于捕获和处理异步处理过程当中出现的异常。

下面来举个粟子:

var promise = new Promise(function(resolve, reject) {

   // 一些异步逻辑,好比ajax, setTimeout等

  if (/* 异步操做成功 */){
    resolve(value);   // 成功则返回结果
  } else {
    reject(error);    // 失败则返回错误
  }
}).then(function(value){
   // 不是想要的结果,继续往下执行
}).then(function(value){
  // 不是想要的结果,继续往下执行
}).then
……

}).then(function(value){
   // 是最终想要的结果
}).catch(function(err){

  throw err;  // 若是有异常则抛出

})

那么,能不能同时执行多个promise实例呢?

能够的,promise.all()方法能够帮到你。

不得不说,promise是解决异步回调的一大进步,是一个很是优秀的解决方案。而因为promise的强大,生态圈出现了不少基于promise的优秀模块, 好比bluebirdq等等。

然而,promise并不是终点,它只是弱化了回调地狱,并不能真正消除回调。使用promise仍然要处理不少复杂的逻辑,以及写不少的逻辑代码

而要消除回调,意味着要实现以同步的方式来写异步编程。

那么如何来实现?

此时舞台再次交给TJ大神,由于他写了个co,利用generator协程机制,实现以同步的方式来写异步编程。

不得不膜拜下TJ大神。

generator 时代

关于generator的相关知识,可参考阮一峰老师写的es6入门之generator

和promise同样,generator一样是es6的新特性,但它并不是为解决回调而存在的,只是它刚好拥有这个能力,而TJ大神看到这种可能,因而他利用generator封装了co。并基于co,他又创造了个更轻量,性能更好的koa1web框架。

自此,koa1终于诞生了!它迎合了es6和co,

koa1和express相比,有很是大的进步,其中之一就是它很大程度上真正地解决了异步回调问题,真正意义上实现同步方式来写异步编程。

再就是,koa1更轻量,性能比express更为优异。

koa1实现同步写异步的关键点就是co。那么,co是如何实现同步写异步的呢?

下面继续来个举个粟子:

# 正常的异步回调
var request = require('request');
var a = {};
var b = {};
request('http://www.google.com', function (error, response, body) {
    if (!error && response.statusCode == 200) {
        a.response = response;
        a.body = body;
        request('http://www.yahoo.com', function (error, response, body) {
            if (!error && response.statusCode == 200) {
                b.response = response;
                b.body = body;
            }
        });
    }
});


# co异步处理

co(function *(){
  var a = yield request('http://google.com');    // 以同步的方式,直接拿到异步结果,并往下执行
  var b = yield request('http://yahoo.com');
  console.log(a[0].statusCode);
  console.log(b[0].statusCode);
})()

看完这个粟子,是否是十分的激动呢?

咱们再来看看,基于co的koa1是如何处理异步的, 一样举个粟子:

# 发布文章接口

const parse = require('co-body');
const mongoose = require('mongoose');
const Post = mongoose.model('Post');

// 发布文章
exports.create = function *() {                        // 使用 *表示这是一个gentator函数
  let post = new Post(this.req.body.post);
  let tags;

  post.set('user_id', this.user.id);

  if (yield post.save()) {                             // yield直接获取异步执行的结果
    this.redirect('/admin/posts');
  } else {
    tags = yield Tag.findAll();
    this.body = yield render('post/new', { post: post.toJSON(), tags: tags.toJSON() });    // yield直接获取异步执行的结果
  }
}

想象一下,这个例子若是使用express来作会是怎样呢?

相信你心中有数,很无情地抛弃了express,express哭晕厕所😢。

下面开始回归正题。

咱们来探讨下,如何使用更好的组织结构,更少的代码量来实现大量api的逻辑层

3, 探讨一,koa1的实现

通过前面的诸多讲述了解到,异步回调这一大难题,到了koa1才真正意义上的得以解决,准确来讲是generator的功劳。

以同步的方式处理异步回调,这才是咱们想要的结果,意味着咱们能够用不多的代码量来实现api的逻辑层。

解决了异步回调后,此时咱们考虑另外一个问题,如何集中处理,暴露大量api逻辑层?

此时,时代进步的利器——es6,排上用场了。

使用es6

这里主要使用es6的几个新特效,export, import等等。

下面,咱们举个粟子来说述:

首先是api接口层

# /server/router.js     // 组织api的接口层

 const router = require('koa-router')();       // koa1.x
 const userctrl = require('../controllers/users/userctrl');   // 引用用户模块逻辑层
 const  articlectrl = require('../controllers/articles/articlectrl');   // 引用文章模块逻辑层

 router
       //  用户模块api
       .post('/api/user/login',userctrl.login)         // 用户登陆

       .post('/api/user/register',userctrl.register)   // 用户注册      

       .put('/api/user/put',userctrl.put)               // 更改用户资料

       .put('/api/user/resetpwd',userctrl.resetpwd)        // 重置用户密码

       .delete('/api/user/deluser',resetpwd.deluser)   // 删除用户

        // 文章模块api
       .post('/api/article/create',articlectrl.create)         // 发布文章

       .get('/api/article/detail',articlectrl.detail)         // 获取文章详情          

       .put('/api/article/put',articlectrl.put)         // 编辑文章

       .delete('/api/article/del',articlectrl.del)         // 删除文章

       .post('/api/article/praise',articlectrl.praise)         // 点赞文章

       .post('/api/article/comments',articlectrl.comments)         // 发布评论

       .delete('/api/article/del_comments',articlectrl.del_comments);         // 删除评论

   export default router;

不知注意到没,用户模块和文章模块都分别只引入了一个文件,分别是userctrl.js和articlectrl.js,全部用户和文章模块相关的api逻辑层都集中在这两个文件中处理。

如何作的呢? 请看下面的粟子:

  • 用户模块
# /controllers/users/userctrl.js

// 用户登陆
exports.login = function *(){
   // yield ....
}

// 用户注册
exports.register = function *(){
   // yield ....
}

// 更改用户资料
exports.put = function *(){
   // yield ....
}

// 重置用户密码
exports.resetpwd = function *(){
   // yield ....
}

// 用户登陆
exports.deluser = function *(){
   // yield ....
}
  • 文章模块
# /controllers/articles/articlectrl.js

// 发布文章
exports.create = function *(){
   // yield ....
}

// 获取文章详情
exports.detail = function *(){
   // yield ....
}

// 编辑文章
exports.put = function *(){
   // yield ....
}

// 删除文章
exports.del = function *(){
   // yield ....
}

// 点赞文章
exports.praise = function *(){
   // yield ....
}

// 发布评论
exports.comments = function *(){
   // yield ....
}

// 删除评论
exports.del_comments = function *(){
   // yield ....
}

到了这一步,api接口层和逻辑层都已处理完毕。

这里有个小问题,使用koa,意味着你须要使用try/catch去捕获内部错误,但若是每一个api都try/catch一遍,那是极其繁琐的,
也会占用很多代码量和空间

对于这个问题,咱们能够把try/catch封装成一个中间件来处理,只须要把这个中间放在路由以前执行便可。对此,能够参考阿里云栖里的这篇文章——如何优雅的在 koa 中处理错误

至此,一个基于koa1+es6的Restful API打造完成。

然而,这仍不是终点。

4. co的末日,也是koa1的末日

co/koa1这么厉害,实现了promise,async都解决不了的同步写异步,为何会是末日呢?

co/koa1并非很差,而是有比它更好的,从而淹没了他们的光芒,所谓壮士一去不复返,垂泪三千尺😢。

是什么抢走了co/koa1的光芒?你应该猜到了,那就是koa2async/await

async/await来势汹汹,它有个代号,叫——终结者。别误会,不是那个酷酷的美国大叔。

async/await并不是第三方实现,而是原生javascript的实现,也就是说它不是bluebird,q,async那一流,未来它是要进入w3c标准的,官方的解决方案。 准确地说,它才是正统皇帝,generator只是代皇帝,bluebird,q,async之类的则只是江湖侠客。

为此,自nodejs发布到7.x之后,TJ 大神推出了koa2,内置co包,直接支持async/await。并将会在koa3中彻底移除对generators的支持。

async/await很是新,它并不属于es6,而是属于es7。和generator同样,它实现了同步写异步,终结异步回调。

而async/await具备很是大的优点,首先它自己是generator语法糖,自带执行器,更具语义化,适用性更广。其次,它并不须要像co这样的第三方实现,而是原生支持的。

那么,使用async/await是怎样的体验呢?以个人开源博客sinn源码为例,下面来个粟子:

// 查询二级文章分类
  static async get_category(ctx) {                           // async声明这是一个async函数
   const data = await CategoryModel.find();          // await 获取异步结果
   if(!data) return ctx.error({msg: '暂无数据'});
   return ctx.success({ data });
  }

  // 查询分类菜单
  static async getmenu_category(ctx) {
   const data = await CategoryModel.find({}).populate('cate_parent');
   if(!data) return ctx.error({msg: '获取菜单失败!'});
   return ctx.success({ data });
  }

5. 探讨二,koa2+es6/7的实现

直接奔入最终主题。

前面讲了koa1+es6实现Restful API的打造,可它并不是是最优解。

真正的最优方案是koa2+async/await+class的实现。

这里为何提到class呢?

class是es6版的面向对象的实现,是的,你没有看错,你曾经所熟悉的oop能够玩起来了。

但是,这里为何须要用到它?

由于,class+async/await的结合,可使你更好的组织api的逻辑层,语义更清晰,结构更清晰,代码量更少更轻,更容易维护。至此,你再也不须要export每一个接口逻辑了。另外一个优势,它一样具备很好的性能。

下面来个真实的粟子,以个人开源博客sinn源码为例:

首先是接口层:

# /server/router.js     // 组织api的接口层

 const router = require('koa-router')(); 
 const userctrl = require('../controllers/users/UserController');   // 引入用户模块逻辑层

 router
         //  用户模块api
       .post('/api/user/login',userctrl.login)         // 用户登陆
       .post('/api/user/register',userctrl.register)   // 用户注册      
       .get('/api/user/logout',userctrl.logout)     // 用户退出      
       .put('/api/user/put',userctrl.put)               // 更改用户资料
       .put('/api/user/resetpwd',userctrl.resetpwd)        // 重置用户密码
       .delete('/api/user/deluser',resetpwd.deluser)   // 删除用户
       ……

而后是逻辑层

# /server/users/UserController.js  用户模块  

import mongoose from 'mongoose';
import md5 from 'md5';
const UserModel = mongoose.model('User');

class UserController {

  // 用户注册
   async register(ctx) {
      // await ……
   }

  // 用户登陆
  async login(ctx) {
   // await ……
  }

   // 用户退出
  async logout(ctx) {
   // await ……
  }

  // 更新用户资料
  async put(ctx) {
   // await ……
  }

  // 删除用户
  async deluser(ctx) {
   // await ……
  }

 // 重置密码
  async resetpwd(ctx) {
   // await ……
  }

  ……

}
export default new UserController();

是否是更清晰,更有结构性了呢?

你甚至还能够用extends(继承)来实现更复杂的api。

可是,不知你有没有注意到一个细节,上面的例子用了new实例化。

实例化,意味着会消耗必定内存,消耗性能。虽然在后端这是种消耗不会很大。

可是做为一名优秀的程序员,咱们尽可能追求极致。

须要明白的一点是,一般咱们的api不会复杂到大量使用oop的知识,好比大量地使用原型,继承来实现复杂的实例,并无,至少后端js逻辑不会是前端那般复杂。

其次,咱们的需求很简单,只须要可以批量定义和导出众多api的逻辑层方法便可。

既然如此,为何不用静态方法呢?是的,static来了。

es6的class中,可用static来定义静态方法,甚至能够定义静态属性(es7才实现)。静态方法并不须要实例化就能够访问,也就意味着,使用static,你不须要new,你能够减小内存的损耗。

下面咱们改造一下上面逻辑层的例子:

class UserController {

  // 用户注册
   static async register(ctx) {
      // await ……
   }

  // 用户登陆
  static async login(ctx) {
   // await ……
  }

   // 用户退出
  static async logout(ctx) {
   // await ……
  }

  // 更新用户资料
  static async put(ctx) {
   // await ……
  }

  // 删除用户
  static async deluser(ctx) {
   // await ……
  }

 // 重置密码
  static async resetpwd(ctx) {
   // await ……
  }

  ……

export default UserController;

}

是否是感受高大上了不少?

另外,还有两点咱们能够优化的。

第一点是,避免在每一个接口逻辑层中使用try/catch,而是封装一个try/catch中间件来处理它们,这样能够减小代码量,工做量,以及减小空间的占用。

第二点是,把一些公共方法抽离出来,一样用class来组织它们,使用也很简单,你能够单独引进,也可使用extends来继承公共方法的class类,以访问父类方法的方式来获取它们。

至此,一个基于koa2+es6/7打造的高质量Restful API终于完成。

总结

若是你正准备学nodejs,除了原生node之外,你能够直接学习和使用koa2。

若是你习惯于express或koa1,也建议迁移到koa2

async/await以及众多es6/7特性的出现,是对nodejs负担的一种释放,你能够很好地利用好它们来提升你的编码效率和质量。

原文:https://zhuanlan.zhihu.com/p/26216336

相关文章
相关标签/搜索