使用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

相關文章
相關標籤/搜索