現在nodejs變得愈來愈火熱,採用nodejs實現先後端分離架構已被多數大公司所採用。javascript
在過去,使用nodejs你們首先想到的是TJ大神寫的express.js,而發展到現在,更輕量,性能更好的koa已然成爲主流,html
它一樣出自TJ大神手筆,現在版本已更新到了koa2,不只性能優異,它還支持async/await,堪稱回調地獄的終結者前端
下面,咱們來探討下,如何使用koa2+es6/7來打造高質量的Restful風格API。java
刨根問底,篇幅略長,精華在後面,須要耐心看。node
一種是耦合模式,即接口層和邏輯層都由一個函數來處理完成。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等等,這很方便於多人協同開發,在大型開發中尤其重要
很顯然,分離模式優於耦合模式。
通過上面的分析以後,咱們選擇更優的分離模式, 它只須要你關注邏輯層。
可是,以上面分離模式的例子爲例,每個接口仍然須要單獨一個js文件來處理它的邏輯層,而且須要用不少不一樣文件夾來組織它
們,假如應用足夠大,有幾十甚至上百個api,那意味着頗有可能你的js邏輯文件也達幾十乃至上百個,而用來劃分和組織這些js文
件的文件夾也不在少數。
這就形成了過於臃腫,難以維護的毛病。
那麼,有沒有可能,一個功能模塊只須要一個js文件來處理它們的全部邏輯層,並更具可維護性呢?
打個比方,如今有一個博客站點,我僅使用一個user.js文件來處理用戶模塊全部api的邏輯層,包括註冊,登陸,修改,刪除,密碼重置等等,另外用一個article.js文件來處理文章模塊全部api的邏輯層,包括髮布,修改,獲取詳情,點贊,評論,刪除等等。
若是能夠作到這樣,那就意味着代碼量大大減小,且可維護性更高。
而要作到這步,咱們須要解決兩個問題,一個是異步回調,由於異步回調使咱們增長了不少代碼量,邏輯複雜,二是如何批量定義和導出大量api的邏輯層方法。
首先,咱們先來解決異步回調這個問題,下面將會展開講解。
爲了減小篇幅,下面只作簡要的淺析。
咱們先來回顧一下歷史。
鑑於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。
這曾是一個很是優秀的第三方模塊,它基於回調機制來實現,是處理異步回調很好的解決方案,現在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有所瞭解,能夠參考阮一峯寫的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的優秀模塊, 好比bluebird, q等等。
然而,promise並不是終點,它只是弱化了回調地獄,並不能真正消除回調。使用promise仍然要處理不少複雜的邏輯,以及寫不少的邏輯代碼
而要消除回調,意味着要實現以同步的方式來寫異步編程。
那麼如何來實現?
此時舞臺再次交給TJ大神,由於他寫了個co,利用generator協程機制,實現以同步的方式來寫異步編程。
不得不膜拜下TJ大神。
關於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的邏輯層
通過前面的諸多講述瞭解到,異步回調這一大難題,到了koa1才真正意義上的得以解決,準確來講是generator的功勞。
以同步的方式處理異步回調,這纔是咱們想要的結果,意味着咱們能夠用不多的代碼量來實現api的邏輯層。
解決了異步回調後,此時咱們考慮另外一個問題,如何集中處理,暴露大量api邏輯層?
此時,時代進步的利器——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打造完成。
然而,這仍不是終點。
co/koa1這麼厲害,實現了promise,async都解決不了的同步寫異步,爲何會是末日呢?
co/koa1並非很差,而是有比它更好的,從而淹沒了他們的光芒,所謂壯士一去不復返,垂淚三千尺😢。
是什麼搶走了co/koa1的光芒?你應該猜到了,那就是koa2、async/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 });
}
直接奔入最終主題。
前面講了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