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


前言

如今nodejs變得越來越火熱,采用nodejs實現前后端分離架構已被多數大公司所采用。

在過去,使用nodejs大家首先想到的是TJ大神寫的express.js,而發展到如今,更輕量,性能更好的koa已然成為主流,

它同樣出自TJ大神手筆,如今版本已更新到了koa2,不僅性能優異,它還支持async/await,堪稱回調地獄的終結者

下面,我們來探討下,如何使用koa2+es6/7來打造高質量的Restful風格API。

刨根問底,篇幅略長,精華在后面,需要耐心看。

1. 兩種模式

一種是耦合模式,即接口層和邏輯層都由一個函數來處理完成。

另一種是分離模式,即接口層和邏輯層是分開的。

下面我們先來說第一種。

耦合模式

先舉個粟子,以express為例:

# /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){

  // 邏輯層

 })

這種在過去很常見,相信很多人都寫過,我也不例外。但並不推薦。

首先,一個應用的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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM