前傳
出于興趣最近開始研究koa2培愁,由于之前有過一些express經(jīng)驗(yàn)井辜,以為koa還是很好上手的绎谦,但是用起來發(fā)現(xiàn)還是有些地方容易懵逼,因此整理此文粥脚,希望能夠幫助到一些新人窃肠。
如果你不懂javascript,建議你先去擼一遍紅寶書javascript高級(jí)程序設(shè)計(jì)
如果你不熟悉ES6阿逃,建議你先去擼一遍阮一峰老師的ECMAScript 6入門
因?yàn)槲乙彩切氯嗣。抑皇钦砹宋业膶W(xué)習(xí)經(jīng)歷,如何填平踩到的坑恃锉。
如果有讀者發(fā)現(xiàn)我有寫錯(cuò)的地方希望你能及時(shí)留言給我搀菩,別讓我誤導(dǎo)了其他新手。
本文的系統(tǒng)環(huán)境Mac OS
編譯器 VScode
1 構(gòu)建項(xiàng)目
想使用koa破托,我們肯定首先想到去官網(wǎng)看看肪跋,沒準(zhǔn)有個(gè)guide之類的能夠輕松入門,可是koa官網(wǎng)跟koa本身一樣簡(jiǎn)潔土砂。
如果要我一點(diǎn)點(diǎn)搭建環(huán)境的話州既,感覺好麻煩,所以先去找了找有沒有項(xiàng)目生成器萝映,然后就發(fā)現(xiàn)了狼叔-桑世龍寫的koa-generator吴叶。
1.1 安裝koa-generator
在終端輸入:
$ npm install -g koa-generator
1.2 使用koa-generator生成koa2項(xiàng)目
在你的工作目錄下,輸入:
$ koa2 HelloKoa2
成功創(chuàng)建項(xiàng)目后序臂,進(jìn)入項(xiàng)目目錄蚌卤,并執(zhí)行<code>npm install</code>命令
$ cd HelloKoa2
$ npm install
1.3 啟動(dòng)項(xiàng)目
在終端輸入:
$ npm start
項(xiàng)目啟動(dòng)后实束,默認(rèn)端口號(hào)是3000,在瀏覽器中運(yùn)行可以得到下圖的效果說明運(yùn)行成功逊彭。
[圖片上傳失敗...(image-aca657-1539076881864)]
在此再次感謝狼叔-桑世龍咸灿。
當(dāng)前項(xiàng)目的文件目錄如下圖
[圖片上傳失敗...(image-c09d4d-1539076881864)]
1.4 關(guān)于koa2
1.4.1 中間件的執(zhí)行順序
koa的中間件是由generator組成的,這決定了中間件的執(zhí)行順序侮叮。
Express的中間件是順序執(zhí)行避矢,從第一個(gè)中間件執(zhí)行到最后一個(gè)中間件,發(fā)出響應(yīng)囊榜。
[圖片上傳失敗...(image-dad050-1539076881864)]
koa是從第一個(gè)中間件開始執(zhí)行审胸,遇到<code>next</code>進(jìn)入下一個(gè)中間件,一直執(zhí)行到最后一個(gè)中間件锦聊,在逆序歹嘹,執(zhí)行上一個(gè)中間件<code>next</code>之后的代碼,一直到第一個(gè)中間件執(zhí)行結(jié)束才發(fā)出響應(yīng)孔庭。
[圖片上傳失敗...(image-d9058f-1539076881864)]
1.4.2 async await語法支持
koa2增加了<code>async</code> <code>await</code>語法的支持.
原來koa的中間件寫法
app.use(function *(next){
var start = new Date;
yield next;
var ms = new Date - start;
this.set('X-Response-Time', ms + 'ms');
});
koa2中的寫法
app.use(async (next) => {
var start = new Date;
await next();
var ms = new Date - start;
this.set('X-Response-Time', ms + 'ms');
});
koa聲明說要在v3版本中取消對(duì)generator中間件的支持尺上,所以為了長(zhǎng)久考慮還是用async語法的好。
如果想要繼續(xù)使用<code>function*</code>語法圆到,可以使用 <code>koa-convert</code> 這個(gè)中間件進(jìn)行轉(zhuǎn)換怎抛。這也是你看到項(xiàng)目中會(huì)有下面代碼的原因
const convert = require('koa-convert');
app.use(convert(bodyparser));
app.use(convert(json()));
app.use(convert(logger()));
1.4.3 Context
Context封裝了node中的request和response。
koa@1.x使用this引用Context對(duì)象:
app.use(function *(){
this.body = 'Hello World';
});
koa@2.x中使用ctx來訪問Context對(duì)象:
app.use(async (ctx, next) => {
await next();
ctx.body = 'Hello World';
});
上面代碼中的<code>ctx.body = 'Hello World'</code>這行代碼表示設(shè)置response.body的值為'Hello World'芽淡。
如果你看文檔就有可能懵逼马绝,那么我發(fā)送post請(qǐng)求的參數(shù)應(yīng)該怎么獲取呢?
貌似ctx不能直接獲取request的body,想要獲取post請(qǐng)求中的參數(shù)要使用<code>ctx.request.body</code>挣菲。
如需查看項(xiàng)目代碼 –> 代碼地址:
https://github.com/tough1985/hello-koa2
選擇Tag -> step1
2 項(xiàng)目配置
這里的配置指的是運(yùn)行環(huán)境的配置富稻,比如我們?cè)陂_發(fā)階段使用本地的數(shù)據(jù)庫,測(cè)試要使用測(cè)試庫白胀,發(fā)布上線時(shí)候使用線上的庫椭赋,也會(huì)有不同的端口號(hào)。
2.1 當(dāng)我們輸入npm start的時(shí)候都干了些什么
在package.json文件中
"scripts": {
"start": "./node_modules/.bin/nodemon bin/run",
"koa": "./node_modules/.bin/runkoa bin/www",
"pm2": "pm2 start bin/run ",
"test": "echo \"Error: no test specified\" && exit 1"
}
可以看到這部分內(nèi)容或杠,當(dāng)我們?cè)诮K端輸入:
$ npm start
在就會(huì)運(yùn)行package.json中scripts對(duì)象對(duì)應(yīng)的start字段后面的內(nèi)容哪怔,相當(dāng)于你在終端輸入:
$ ./node_modules/.bin/nodemon bin/run
nodemon插件的作用是在你啟動(dòng)了服務(wù)之后,修改文件可以自動(dòng)重啟服務(wù)向抢。
關(guān)于nodemon的更多內(nèi)容 --> nodemon
如果不考慮自動(dòng)重啟功能认境,其實(shí)這句代碼相當(dāng)于執(zhí)行了<code>node bin/run</code>
我們可以看到項(xiàng)目的bin目錄下,有一個(gè)run文件挟鸠,代碼如下:
#!/usr/bin/env node
var current_path = process.cwd();
require('runkoa')(current_path + '/bin/www' )
這里引入了一個(gè)runkoa叉信,這個(gè)組件是狼叔寫的koa2對(duì)babel環(huán)境依賴的一個(gè)封裝插件。
關(guān)于runkoa相關(guān)內(nèi)容說明 --> runkoa艘希。這里我們最終會(huì)執(zhí)行bin目錄下的www文件來啟動(dòng)服務(wù)茉盏。
2.2 npm scripts
我們?cè)?strong>scripts對(duì)象中添加一段代碼"start_koa": "bin/run"
鉴未,修改后scripts對(duì)象的內(nèi)容如下:
"scripts": {
"start": "./node_modules/.bin/nodemon bin/run",
"koa": "./node_modules/.bin/runkoa bin/www",
"pm2": "pm2 start bin/run ",
"test": "echo \"Error: no test specified\" && exit 1",
"start_koa": "bin/run"
}
那么既然輸入<code>npm start</code>執(zhí)行start后面的腳本枢冤,聰明的你一定會(huì)想:是不是我輸入<code>npm start_koa</code>就可以執(zhí)行start_koa后面相關(guān)的代碼了呢鸠姨?
不管你是怎么想的,反正我當(dāng)時(shí)就想的這么天真淹真。
事實(shí)上我們輸入<code>npm start_koa</code>之后讶迁,終端會(huì)提示npm沒有相關(guān)的命令。
那么在scripts中的start_koa命令要怎么使用呢核蘸,其實(shí)要加一個(gè)run命令才能執(zhí)行巍糯,在終端輸入:
$ npm run start_koa
可以看到服務(wù)正常運(yùn)行了。
在npm中客扎,有四個(gè)常用的縮寫
npm start是npm run start
npm stop是npm run stop的簡(jiǎn)寫
npm test是npm run test的簡(jiǎn)寫
npm restart是npm run stop && npm run restart && npm run start的簡(jiǎn)寫
其他的都要使用<code>npm run</code>來執(zhí)行了祟峦。
推薦讀一遍阮一峰老師寫的npm scripts 使用指南,很有幫助徙鱼。
2.3 配置環(huán)境
關(guān)于配置環(huán)境常用的有development宅楞、test、production袱吆、debug厌衙。
可以使用node提供的<code>process.env.NODE_ENV</code>來設(shè)置。
在啟動(dòng)服務(wù)的時(shí)候可以對(duì)NODE_ENV進(jìn)行賦值绞绒,例如:
$ NODE_ENV=test npm start
然后我們可以在bin/www文件中輸出一下婶希,看看是否配置成功,添加如下代碼:
console.log("process.env.NODE_ENV=" + process.env.NODE_ENV);
然后在終端輸入
$ NODE_ENV=test npm start
可以看到終端打优詈狻:
process.env.NODE_ENV=test
我們可以在scripts對(duì)象中將環(huán)境配置好喻杈,例如我們將start和test分別設(shè)置development和test環(huán)境,代碼如下:
"scripts": {
"start": "NODE_ENV=development ./node_modules/.bin/nodemon bin/run",
"koa": "./node_modules/.bin/runkoa bin/www",
"pm2": "pm2 start bin/run ",
"test": "NODE_ENV=test echo \"Error: no test specified\" && exit 1",
"start_koa": "bin/run"
},
可以在終端分別輸入<code>npm start</code>和<code>npm test</code>來測(cè)試環(huán)境配置是否生效狰晚。
由于并沒有測(cè)試內(nèi)容筒饰,現(xiàn)在的test腳本會(huì)退出,后面我們?cè)谠斦刱oa的測(cè)試家肯。
2.4 配置文件
為了能夠根據(jù)不同的運(yùn)行環(huán)境加載不同的配置內(nèi)容龄砰,我們需要添加一些配置文件。
首先在項(xiàng)目根目錄下添加config目錄讨衣,在config目錄下添加index.js换棚、test.js、development.js三個(gè)文件反镇,內(nèi)容如下固蚤。
development.js
/**
* 開發(fā)環(huán)境的配置內(nèi)容
*/
module.exports = {
env: 'development', //環(huán)境名稱
port: 3001, //服務(wù)端口號(hào)
mongodb_url: '', //數(shù)據(jù)庫地址
redis_url:'', //redis地址
redis_port: '' //redis端口號(hào)
}
test.js
/**
* 測(cè)試環(huán)境的配置內(nèi)容
*/
module.exports = {
env: 'test', //環(huán)境名稱
port: 3002, //服務(wù)端口號(hào)
mongodb_url: '', //數(shù)據(jù)庫地址
redis_url:'', //redis地址
redis_port: '' //redis端口號(hào)
}
index.js
var development_env = require('./development');
var test_env = require('./test');
//根據(jù)不同的NODE_ENV,輸出不同的配置對(duì)象歹茶,默認(rèn)輸出development的配置對(duì)象
module.exports = {
development: development_env,
test: test_env
}[process.env.NODE_ENV || 'development']
代碼應(yīng)該都沒什么可解釋的夕玩,然后我們?cè)賮砭庉?em>bin/www文件你弦。
bin/www添加如下代碼
//引入配置文件
var config = require('../config');
// 將端口號(hào)設(shè)置為配置文件的端口號(hào),默認(rèn)值為3000
var port = normalizePort(config.port || '3000');
// 打印輸出端口號(hào)
console.log('port = ' + config.port);
測(cè)試效果燎孟,在終端輸入<code>npm start</code>禽作,可以看到
process.env.NODE_ENV=development
port = 3001
到瀏覽器中訪問http://127.0.0.1:3001,可以看到原來的輸入內(nèi)容揩页,說明配置文件已經(jīng)生效旷偿。
如需查看項(xiàng)目代碼 –> 代碼地址:
https://github.com/tough1985/hello-koa2
選擇Tag -> step2
3 日志
狼叔的koa-generator已經(jīng)添加了koa-logger,在app.js文件你可以找到這樣的代碼:
const logger = require('koa-logger');
...
...
app.use(convert(logger()));
koa-logger是tj大神寫的koa開發(fā)時(shí)替換console.log輸出的一個(gè)插件爆侣。
如果你需要按照時(shí)間或者按照文件大小萍程,本地輸出log文件的話,建議還是采用log4js-node兔仰。
3.1 log4js
log4js提供了多個(gè)日志等級(jí)分類茫负,同時(shí)也能替換console.log輸出,另外他還可以按照文件大小或者日期來生成本地日志文件乎赴,還可以使用郵件等形式發(fā)送日志忍法。
我們?cè)谶@演示用info和error兩種日志等級(jí)分別記錄響應(yīng)日志和錯(cuò)誤日志。
3.2 log4js 配置
在config目錄下創(chuàng)建一個(gè)log_config.js文件无虚,內(nèi)容如下:
var path = require('path');
//錯(cuò)誤日志輸出完整路徑
var errorLogPath = path.resolve(__dirname, "../logs/error/error");
//響應(yīng)日志輸出完整路徑
var responseLogPath = path.resolve(__dirname, "../logs/response/response");
module.exports = {
"appenders":
[
//錯(cuò)誤日志
{
"category":"errorLogger", //logger名稱
"type": "dateFile", //日志類型
"filename": errorLogPath, //日志輸出位置
"alwaysIncludePattern":true, //是否總是有后綴名
"pattern": "-yyyy-MM-dd-hh.log" //后綴缔赠,每小時(shí)創(chuàng)建一個(gè)新的日志文件
},
//響應(yīng)日志
{
"category":"resLogger",
"type": "dateFile",
"filename": responseLogPath,
"alwaysIncludePattern":true,
"pattern": "-yyyy-MM-dd-hh.log"
}
],
"levels": //設(shè)置logger名稱對(duì)應(yīng)的的日志等級(jí)
{
"errorLogger":"ERROR",
"resLogger":"ALL"
}
}
然后創(chuàng)建一個(gè)utils目錄,添加log_util.js文件友题,內(nèi)容如下:
var log4js = require('log4js');
var log_config = require('../config/log_config');
//加載配置文件
log4js.configure(log_config);
var logUtil = {};
var errorLogger = log4js.getLogger('errorLogger');
var resLogger = log4js.getLogger('resLogger');
//封裝錯(cuò)誤日志
logUtil.logError = function (ctx, error, resTime) {
if (ctx && error) {
errorLogger.error(formatError(ctx, error, resTime));
}
};
//封裝響應(yīng)日志
logUtil.logResponse = function (ctx, resTime) {
if (ctx) {
resLogger.info(formatRes(ctx, resTime));
}
};
//格式化響應(yīng)日志
var formatRes = function (ctx, resTime) {
var logText = new String();
//響應(yīng)日志開始
logText += "\n" + "*************** response log start ***************" + "\n";
//添加請(qǐng)求日志
logText += formatReqLog(ctx.request, resTime);
//響應(yīng)狀態(tài)碼
logText += "response status: " + ctx.status + "\n";
//響應(yīng)內(nèi)容
logText += "response body: " + "\n" + JSON.stringify(ctx.body) + "\n";
//響應(yīng)日志結(jié)束
logText += "*************** response log end ***************" + "\n";
return logText;
}
//格式化錯(cuò)誤日志
var formatError = function (ctx, err, resTime) {
var logText = new String();
//錯(cuò)誤信息開始
logText += "\n" + "*************** error log start ***************" + "\n";
//添加請(qǐng)求日志
logText += formatReqLog(ctx.request, resTime);
//錯(cuò)誤名稱
logText += "err name: " + err.name + "\n";
//錯(cuò)誤信息
logText += "err message: " + err.message + "\n";
//錯(cuò)誤詳情
logText += "err stack: " + err.stack + "\n";
//錯(cuò)誤信息結(jié)束
logText += "*************** error log end ***************" + "\n";
return logText;
};
//格式化請(qǐng)求日志
var formatReqLog = function (req, resTime) {
var logText = new String();
var method = req.method;
//訪問方法
logText += "request method: " + method + "\n";
//請(qǐng)求原始地址
logText += "request originalUrl: " + req.originalUrl + "\n";
//客戶端ip
logText += "request client ip: " + req.ip + "\n";
//開始時(shí)間
var startTime;
//請(qǐng)求參數(shù)
if (method === 'GET') {
logText += "request query: " + JSON.stringify(req.query) + "\n";
// startTime = req.query.requestStartTime;
} else {
logText += "request body: " + "\n" + JSON.stringify(req.body) + "\n";
// startTime = req.body.requestStartTime;
}
//服務(wù)器響應(yīng)時(shí)間
logText += "response time: " + resTime + "\n";
return logText;
}
module.exports = logUtil;
接下來修改app.js 文件中的logger部分嗤堰。
//log工具
const logUtil = require('./utils/log_util');
// logger
app.use(async (ctx, next) => {
//響應(yīng)開始時(shí)間
const start = new Date();
//響應(yīng)間隔時(shí)間
var ms;
try {
//開始進(jìn)入到下一個(gè)中間件
await next();
ms = new Date() - start;
//記錄響應(yīng)日志
logUtil.logResponse(ctx, ms);
} catch (error) {
ms = new Date() - start;
//記錄異常日志
logUtil.logError(ctx, error, ms);
}
});
在這將<code>await next();</code>放到了一個(gè)<code>try catch</code>里面,這樣后面的中間件有異常都可以在這集中處理度宦。
比如你會(huì)將一些API異常作為正常值返回給客戶端踢匣,就可以在這集中進(jìn)行處理。然后后面的中間件只要<code>throw</code>自定義的API異常就可以了戈抄。
在啟動(dòng)服務(wù)之前不要忘記先安裝log4js插件:
$ npm install log4js --save
啟動(dòng)服務(wù)
$ npm start
這時(shí)候會(huì)啟動(dòng)失敗离唬,控制臺(tái)會(huì)輸出沒有文件或文件目錄。原因是我們?cè)谂渲美锩骐m然配置了文件目錄划鸽,但是并沒有創(chuàng)建相關(guān)目錄输莺,解決的辦法是手動(dòng)創(chuàng)建相關(guān)目錄,或者在服務(wù)啟動(dòng)的時(shí)候裸诽,確認(rèn)一下目錄是否存在嫂用,如果不存在則創(chuàng)建相關(guān)目錄。
3.3 初始化logs文件目錄
先來修改一下log_config.js文件丈冬,讓后面的創(chuàng)建過程更舒適嘱函。
修改后的代碼:
var path = require('path');
//日志根目錄
var baseLogPath = path.resolve(__dirname, '../logs')
//錯(cuò)誤日志目錄
var errorPath = "/error";
//錯(cuò)誤日志文件名
var errorFileName = "error";
//錯(cuò)誤日志輸出完整路徑
var errorLogPath = baseLogPath + errorPath + "/" + errorFileName;
// var errorLogPath = path.resolve(__dirname, "../logs/error/error");
//響應(yīng)日志目錄
var responsePath = "/response";
//響應(yīng)日志文件名
var responseFileName = "response";
//響應(yīng)日志輸出完整路徑
var responseLogPath = baseLogPath + responsePath + "/" + responseFileName;
// var responseLogPath = path.resolve(__dirname, "../logs/response/response");
module.exports = {
"appenders":
[
//錯(cuò)誤日志
{
"category":"errorLogger", //logger名稱
"type": "dateFile", //日志類型
"filename": errorLogPath, //日志輸出位置
"alwaysIncludePattern":true, //是否總是有后綴名
"pattern": "-yyyy-MM-dd-hh.log", //后綴,每小時(shí)創(chuàng)建一個(gè)新的日志文件
"path": errorPath //自定義屬性埂蕊,錯(cuò)誤日志的根目錄
},
//響應(yīng)日志
{
"category":"resLogger",
"type": "dateFile",
"filename": responseLogPath,
"alwaysIncludePattern":true,
"pattern": "-yyyy-MM-dd-hh.log",
"path": responsePath
}
],
"levels": //設(shè)置logger名稱對(duì)應(yīng)的的日志等級(jí)
{
"errorLogger":"ERROR",
"resLogger":"ALL"
},
"baseLogPath": baseLogPath //logs根目錄
}
然后打開bin/www文件往弓,添加如下代碼:
var fs = require('fs');
var logConfig = require('../config/log_config');
/**
* 確定目錄是否存在疏唾,如果不存在則創(chuàng)建目錄
*/
var confirmPath = function(pathStr) {
if(!fs.existsSync(pathStr)){
fs.mkdirSync(pathStr);
console.log('createPath: ' + pathStr);
}
}
/**
* 初始化log相關(guān)目錄
*/
var initLogPath = function(){
//創(chuàng)建log的根目錄'logs'
if(logConfig.baseLogPath){
confirmPath(logConfig.baseLogPath)
//根據(jù)不同的logType創(chuàng)建不同的文件目錄
for(var i = 0, len = logConfig.appenders.length; i < len; i++){
if(logConfig.appenders[i].path){
confirmPath(logConfig.baseLogPath + logConfig.appenders[i].path);
}
}
}
}
initLogPath();
這樣每次啟動(dòng)服務(wù)的時(shí)候,都會(huì)去確認(rèn)一下相關(guān)的文件目錄是否存在函似,如果不存在就創(chuàng)建相關(guān)的文件目錄槐脏。
現(xiàn)在在來啟動(dòng)服務(wù)。在瀏覽器訪問缴淋,可以看到項(xiàng)目中多了logs目錄以及相關(guān)子目錄准给,并產(chǎn)生了日子文件。
[圖片上傳失敗...(image-2c145-1539076881864)]
內(nèi)容如下:
[2016-10-31 12:58:48.832] [INFO] resLogger -
*************** response log start ***************
request method: GET
request originalUrl: /
request client ip: ::ffff:127.0.0.1
request query: {}
response time: 418
response status: 200
response body:
"<!DOCTYPE html><html><head><title>koa2 title</title><link rel=\"stylesheet\" href=\"/stylesheets/style.css\"></head><body><h1>koa2 title</h1><p>Welcome to koa2 title</p></body></html>"
*************** response log end ***************
可以根據(jù)自己的需求重抖,定制相關(guān)的日志格式。
另外關(guān)于配置文件的選項(xiàng)可以參考log4js-node Appenders說明祖灰。
如需查看項(xiàng)目代碼 –> 代碼地址:
https://github.com/tough1985/hello-koa2
選擇Tag -> step3
4 格式化輸出
假設(shè)我們現(xiàn)在開發(fā)的是一個(gè)API服務(wù)接口钟沛,會(huì)有一個(gè)統(tǒng)一的響應(yīng)格式,同時(shí)也希望發(fā)生API錯(cuò)誤時(shí)統(tǒng)一錯(cuò)誤格式局扶。
4.1 建立一個(gè)API接口
為當(dāng)前的服務(wù)添加兩個(gè)接口恨统,一個(gè)getUser一個(gè)registerUser。
先在當(dāng)前項(xiàng)目下創(chuàng)建一個(gè)app/controllers目錄三妈,在該目錄下添加一個(gè)user_controller.js文件畜埋。
[圖片上傳失敗...(image-794d63-1539076881864)]
代碼如下:
//獲取用戶
exports.getUser = async (ctx, next) => {
ctx.body = {
username: '阿,希爸',
age: 30
}
}
//用戶注冊(cè)
exports.registerUser = async (ctx, next) => {
console.log('registerUser', ctx.request.body);
}
簡(jiǎn)單的模擬一下畴蒲。getUser返回一個(gè)user對(duì)象悠鞍,registerUser只是打印輸出一下請(qǐng)求參數(shù)。
接下來為這兩個(gè)方法配置路由模燥。
4.2 為API接口配置路由
我們希望服務(wù)的地址的組成是這要的
域名 + 端口號(hào) /api/功能類型/具體端口
例如
127.0.0.1:3001/api/users/getUser
先來添加一個(gè)api的路由和其他路由分開管理咖祭。在routes目錄下創(chuàng)建一個(gè)api目錄,添加user_router.js文件蔫骂,代碼如下:
var router = require('koa-router')();
var user_controller = require('../../app/controllers/user_controller');
router.get('/getUser', user_controller.getUser);
router.post('/registerUser', user_controller.registerUser);
module.exports = router;
這樣就完成了getUser和registerUser進(jìn)行了路由配置么翰,其中getUser是GET方式請(qǐng)求,registerUser是用POST方式請(qǐng)求辽旋。
接下來對(duì)users這個(gè)功能模塊進(jìn)行路由配置浩嫌,在routes/api目錄下添加一個(gè)index.js文件,代碼如下:
var router = require('koa-router')();
var user_router = require('./user_router');
router.use('/users', user_router.routes(), user_router.allowedMethods());
module.exports = router;
最后對(duì)api進(jìn)行路由配置补胚,在app.js文件中添加如下代碼:
const api = require('./routes/api');
......
router.use('/api', api.routes(), api.allowedMethods());
啟動(dòng)服務(wù)码耐,在瀏覽器中訪問127.0.0.1:3001/api/users/getUser可以得到如下輸出,說明配置成功糖儡。
{
"username": "阿伐坏,希爸",
"age": 30
}
4.3 格式化輸出
作為一個(gè)API接口,我們可能希望統(tǒng)一返回格式握联,例如getUser的輸出給客戶端的返回值是這樣的:
{
"code": 0,
"message": "成功",
"data": {
"username": "阿桦沉,希爸",
"age": 30
}
}
按照koa的中間件執(zhí)行順序每瞒,我們要處理數(shù)據(jù)應(yīng)該在發(fā)送響應(yīng)之前和路由得到數(shù)據(jù)之后添加一個(gè)中間件。在項(xiàng)目的根目錄下添加一個(gè)middlewares目錄纯露,在該目錄下添加response_formatter.js文件剿骨,內(nèi)容如下:
/**
* 在app.use(router)之前調(diào)用
*/
var response_formatter = async (ctx, next) => {
//先去執(zhí)行路由
await next();
//如果有返回?cái)?shù)據(jù),將返回?cái)?shù)據(jù)添加到data中
if (ctx.body) {
ctx.body = {
code: 0,
message: 'success',
data: ctx.body
}
} else {
ctx.body = {
code: 0,
message: 'success'
}
}
}
module.exports = response_formatter;
然后在app.js中載入埠褪。
const response_formatter = require('./middlewares/response_formatter');
...
//添加格式化處理響應(yīng)結(jié)果的中間件浓利,在添加路由之前調(diào)用
app.use(response_formatter);
router.use('/', index.routes(), index.allowedMethods());
router.use('/users', users.routes(), users.allowedMethods());
router.use('/api', api.routes(), api.allowedMethods());
app.use(router.routes(), router.allowedMethods());
啟動(dòng)服務(wù),在瀏覽器中訪問127.0.0.1:3001/api/users/getUser可以得到如下輸出钞速,說明配置成功贷掖。
{
"code": 0,
"message": "success",
"data": {
"username": "阿,希爸",
"age": 30
}
}
4.4 對(duì)URL進(jìn)行過濾
為什么一定要在router之前設(shè)置渴语?
其實(shí)在router之后設(shè)置也可以苹威,但是必須在controller里面執(zhí)行<code>await next()</code>才會(huì)調(diào)用。也就是說誰需要格式化輸出結(jié)果自己手動(dòng)調(diào)用驾凶。
在router前面設(shè)置也有一個(gè)問題牙甫,就是所有的路由響應(yīng)輸出都會(huì)進(jìn)行格式化輸出,這顯然也不符合預(yù)期调违,那么我們要對(duì)URL進(jìn)行過濾窟哺,通過過濾的才對(duì)他進(jìn)行格式化處理。
重新改造一下response_formatter中間件技肩,讓他接受一個(gè)參數(shù)且轨,然后返回一個(gè)async function做為中間件。改造后的代碼如下:
/**
* 在app.use(router)之前調(diào)用
*/
var response_formatter = (ctx) => {
//如果有返回?cái)?shù)據(jù)亩鬼,將返回?cái)?shù)據(jù)添加到data中
if (ctx.body) {
ctx.body = {
code: 0,
message: 'success',
data: ctx.body
}
} else {
ctx.body = {
code: 0,
message: 'success'
}
}
}
var url_filter = function(pattern){
return async function(ctx, next){
var reg = new RegExp(pattern);
//先去執(zhí)行路由
await next();
//通過正則的url進(jìn)行格式化處理
if(reg.test(ctx.originalUrl)){
response_formatter(ctx);
}
}
}
module.exports = url_filter;
app.js中對(duì)應(yīng)的代碼改為:
//僅對(duì)/api開頭的url進(jìn)行格式化處理
app.use(response_formatter('^/api'));
現(xiàn)在訪問127.0.0.1:3001/api/users/getUser這樣以api開頭的地址都會(huì)進(jìn)行格式化處理殖告,而其他的地址則不會(huì)。
4.5 API異常處理
要集中處理API異常雳锋,首先要?jiǎng)?chuàng)建一個(gè)API異常類黄绩,在app目錄下新建一個(gè)error目錄,添加ApiError.js文件玷过,代碼如下:
/**
* 自定義Api異常
*/
class ApiError extends Error{
//構(gòu)造方法
constructor(error_name爽丹, error_code, error_message){
super();
this.name = error_name;
this.code = error_code;
this.message = error_message;
}
}
module.exports = ApiError;
為了讓自定義Api異常能夠更好的使用,我們創(chuàng)建一個(gè)ApiErrorNames.js文件來封裝API異常信息辛蚊,并可以通過API錯(cuò)誤名稱獲取異常信息粤蝎。代碼如下:
/**
* API錯(cuò)誤名稱
*/
var ApiErrorNames = {};
ApiErrorNames.UNKNOW_ERROR = "unknowError";
ApiErrorNames.USER_NOT_EXIST = "userNotExist";
/**
* API錯(cuò)誤名稱對(duì)應(yīng)的錯(cuò)誤信息
*/
const error_map = new Map();
error_map.set(ApiErrorNames.UNKNOW_ERROR, { code: -1, message: '未知錯(cuò)誤' });
error_map.set(ApiErrorNames.USER_NOT_EXIST, { code: 101, message: '用戶不存在' });
//根據(jù)錯(cuò)誤名稱獲取錯(cuò)誤信息
ApiErrorNames.getErrorInfo = (error_name) => {
var error_info;
if (error_name) {
error_info = error_map.get(error_name);
}
//如果沒有對(duì)應(yīng)的錯(cuò)誤信息,默認(rèn)'未知錯(cuò)誤'
if (!error_info) {
error_name = UNKNOW_ERROR;
error_info = error_map.get(error_name);
}
return error_info;
}
module.exports = ApiErrorNames;
修改ApiError.js文件袋马,引入ApiErrorNames
ApiError.js
const ApiErrorNames = require('./ApiErrorNames');
/**
* 自定義Api異常
*/
class ApiError extends Error{
//構(gòu)造方法
constructor(error_name){
super();
var error_info = ApiErrorNames.getErrorInfo(error_name);
this.name = error_name;
this.code = error_info.code;
this.message = error_info.message;
}
}
module.exports = ApiError;
在response_formatter.js文件中處理API異常初澎。
先引入ApiError:
<code>var ApiError = require('../app/error/ApiError');</code>
然后修改url_filter
var url_filter = (pattern) => {
return async (ctx, next) => {
var reg = new RegExp(pattern);
try {
//先去執(zhí)行路由
await next();
} catch (error) {
//如果異常類型是API異常并且通過正則驗(yàn)證的url,將錯(cuò)誤信息添加到響應(yīng)體中返回。
if(error instanceof ApiError && reg.test(ctx.originalUrl)){
ctx.status = 200;
ctx.body = {
code: error.code,
message: error.message
}
}
//繼續(xù)拋碑宴,讓外層中間件處理日志
throw error;
}
//通過正則的url進(jìn)行格式化處理
if(reg.test(ctx.originalUrl)){
response_formatter(ctx);
}
}
}
解釋一下這段代碼
使用<code>try catch</code>包裹<code>await next();</code>软啼,這樣后面的中間件拋出的異常都可以在這幾集中處理;
<code>throw error;</code>是為了讓外層的logger中間件能夠處理日志延柠。
為了模擬運(yùn)行效果祸挪,我們修改user_controller.js文件,內(nèi)容如下:
const ApiError = require('../error/ApiError');
const ApiErrorNames = require('../error/ApiErrorNames');
//獲取用戶
exports.getUser = async (ctx, next) => {
//如果id != 1拋出API 異常
if(ctx.query.id != 1){
throw new ApiError(ApiErrorNames.USER_NOT_EXIST);
}
ctx.body = {
username: '阿贞间,希爸',
age: 30
}
}
啟動(dòng)服務(wù)贿条,在瀏覽器中訪問127.0.0.1:3001/api/users/getUser可以得到結(jié)果如下:
{
"code": 101,
"message": "用戶不存在"
}
在瀏覽器中訪問127.0.0.1:3001/api/users/getUser?id=1可以得到結(jié)果如下:
{
"code": 0,
"message": "success",
"data": {
"username": "阿,希爸",
"age": 30
}
}
如需查看項(xiàng)目代碼 –> 代碼地址:
https://github.com/tough1985/hello-koa2
選擇Tag -> step4
5 測(cè)試
node使用主流的測(cè)試框架基本就是mocha和AVA了增热,這里主要以mocha為基礎(chǔ)進(jìn)行構(gòu)建相關(guān)的測(cè)試整以。
5.1 mocha
安裝mocha
在終端輸入
$ npm install --save-dev mocha
--dev表示只在development環(huán)境下添加依賴。
使用mocha
在項(xiàng)目的根目錄下添加test目錄钓葫,添加一個(gè)test.js文件悄蕾,內(nèi)容如下:
var assert = require('assert');
/**
* describe 測(cè)試套件 test suite 表示一組相關(guān)的測(cè)試
* it 測(cè)試用例 test case 表示一個(gè)單獨(dú)的測(cè)試
* assert 斷言 表示對(duì)結(jié)果的預(yù)期
*/
describe('Array', function() {
describe('#indexOf()', function() {
it('should return -1 when the value is not present', function(){
assert.equal(-1, [1,2,3].indexOf(4));
})
})
});
在終端輸入:
$ mocha
可以得到輸出如下:
Array
#indexOf()
? should return -1 when the value is not present
1 passing (9ms)
mocha默認(rèn)運(yùn)行test目錄下的測(cè)試文件,測(cè)試文件一般與要測(cè)試的腳步文件同名以<code>.test.js</code>作為后綴名础浮。例如add.js
的測(cè)試腳本名字就是add.test.js
。
describe表示測(cè)試套件奠骄,每個(gè)測(cè)試腳本至少應(yīng)該包含一個(gè)<code>describe</code>豆同。
it表示測(cè)試用例。
每個(gè)describe可以包含多個(gè)describe或多個(gè)it含鳞。
assert是node提供的斷言庫影锈。
assert.equal(-1, [1,2,3].indexOf(4));
這句代碼的意思是我們期望[1,2,3].indexOf(4)
的值應(yīng)該是-1
,如果[1,2,3].indexOf(4)
的運(yùn)行結(jié)果是-1
蝉绷,則通過測(cè)試鸭廷,否則不通過。
可以把-1
改成-2
再試一下熔吗。
上面的例子是mocha提供的辆床,mocha官網(wǎng)。
測(cè)試環(huán)境
之前說過環(huán)境配置的內(nèi)容桅狠,我們需要執(zhí)行測(cè)試的時(shí)候讼载,加載相關(guān)的測(cè)試配置該怎么做?
在終端輸入
$ NODE_ENV=test mocha
為了避免每次都去輸入NODE_ENV=test
中跌,可以修改package.json文件中的scripts.test
改為:
"test": "NODE_ENV=test mocha",
以后運(yùn)行測(cè)試直接輸入npm test
就可以了咨堤。
常用的參數(shù)
mocha在執(zhí)行時(shí)可以攜帶很多參數(shù),這里介紹幾個(gè)常用的漩符。
--recursive
mocha默認(rèn)執(zhí)行test目錄下的測(cè)試腳本一喘,但是不會(huì)運(yùn)行test下的子目錄中的腳本。
想要執(zhí)行子目錄中的測(cè)試腳本嗜暴,可以在運(yùn)行時(shí)添加--recursive
參數(shù)凸克。
$ mocha --recursive
--grep
如果你寫了很多測(cè)試用例议蟆,當(dāng)你添加了一個(gè)新的測(cè)試,執(zhí)行之后要在結(jié)果里面找半天触徐。這種情況就可以考慮--grep
參數(shù)咪鲜。
--grep
可以只執(zhí)行單個(gè)測(cè)試用例,也就是執(zhí)行某一個(gè)it
撞鹉。比如將剛才的測(cè)試修改如下:
describe('Array', function() {
describe('#indexOf()', function() {
it('should return -1 when the value is not present', function(){
assert.equal(-1, [1,2,3].indexOf(4));
})
it('length', function(){
assert.equal(3, [1, 2, 3].length);
})
})
});
添加了一個(gè)length測(cè)試用例疟丙,想要單獨(dú)執(zhí)行這個(gè)測(cè)試用例就要在終端輸入:
$ mocha --grep 'length'
可以看到length用例被單獨(dú)執(zhí)行了。
這里有一點(diǎn)需要注意鸟雏,因?yàn)槲覀兣渲昧?code>npm test享郊,如果直接運(yùn)行
$ npm test --grep 'length'
這樣是不能達(dá)到效果的。
要給npm scripts腳本傳參需要先輸入--
然后在輸入?yún)?shù)孝鹊,所以想要執(zhí)行上面的效果應(yīng)該輸入:
$ npm test -- --grep 'length'
關(guān)于mocha就簡(jiǎn)單的介紹這么多炊琉,想要了解更多相關(guān)的內(nèi)容,推薦仔細(xì)閱讀一遍阮一峰老師寫的測(cè)試框架 Mocha 實(shí)例教程又活。
5.2 chai
chai是一個(gè)斷言庫苔咪。之前的例子中,我們使用的是node提供的斷言庫柳骄,他的功能比較少团赏,基本上只有equal、ok耐薯、fail這樣簡(jiǎn)單的功能舔清,很難滿足日常的需求。
mocha官方表示你愛用什么斷言用什么斷言曲初,反正老子都支持体谒。
選擇chai是因?yàn)樗麑?duì)斷言的幾種語法都支持,而且功能也比較全面 --> chai官網(wǎng)臼婆。
chai支持should
抒痒、expect
和assert
三種斷言形式。
assert語法之前我們已經(jīng)見過了目锭,chai只是豐富了功能评汰,語法并沒有變化。
expect和should的語法更接近自然語言的習(xí)慣痢虹,但是should使用的時(shí)候會(huì)出現(xiàn)一些意想不到的情況被去。所以比較常用的還是expect。
官方的DEMO
var expect = chai.expect;
expect(foo).to.be.a('string');
expect(foo).to.equal('bar');
expect(foo).to.have.length(3);
expect(tea).to.have.property('flavors')
.with.length(3);
明顯語法的可讀性更好奖唯,更接近人類的語言惨缆。
簡(jiǎn)單的解釋其中的to
、be
這樣的語法。
chai使用了鏈?zhǔn)秸Z法坯墨,為了使語法更加接近自然語言寂汇,添加了很多表達(dá)語義但是沒有任何功能的詞匯。
- to
- be
- been
- is
- that
- which
- and
- has
- have
- with
- at
- of
- same
上面列出的這些詞沒有任何功能捣染,只是為了增強(qiáng)語義骄瓣。
也就是說
expect(1+1).to.be.equal(2)
與
expect(1+1).equal(2)
是完全相同的。
安裝chai
在終端輸入:
$ npm install --save-dev chai
使用chai
在test目錄下新建一個(gè)chai.test.js文件耍攘,內(nèi)容如下:
const expect = require('chai').expect;
describe('chai expect demo', function() {
it('expect equal', function() {
expect(1+1).to.equal(2);
expect(1+1).not.equal(3);
});
});
在終端輸入:
$ npm test -- --grep 'expect equal'
得到輸出:
chai expect demo
? expect equal
1 passing (6ms)
說明配置成功榕栏。有關(guān)chai的更多功能請(qǐng)查看官方API --> chai_api
5.3 supertest
目前我們可以使用測(cè)試框架做一些簡(jiǎn)單的測(cè)試,想要測(cè)試接口的相應(yīng)數(shù)據(jù)蕾各,就要用到supertest了扒磁。
supertest主要功能就是對(duì)HTTP進(jìn)行測(cè)試。尤其是對(duì)REST API式曲,我們對(duì)get請(qǐng)求很容易模擬妨托,但是post方法就很難(當(dāng)然你也可以使用postman這樣的插件)。
supertest可以模擬HTTP的各種請(qǐng)求吝羞,設(shè)置header兰伤,添加請(qǐng)求數(shù)據(jù),并對(duì)響應(yīng)進(jìn)行斷言钧排。
安裝supertest
在終端輸入:
$ npm install --save-dev supertest
使用supertest
我們對(duì)現(xiàn)有的兩個(gè)API接口getUser和registerUser進(jìn)行測(cè)試医清。在test目錄下創(chuàng)建user_api.test.js文件,內(nèi)容如下:
const request = require('supertest');
const expect = require('chai').expect;
const app = require('../app.js');
describe('user_api', () => {
it('getUser', (done) => {
request(app.listen())
.get('/api/users/getUser?id=1') //get方法
.expect(200) //斷言狀態(tài)碼為200
.end((err, res) => {
console.log(res.body);
//斷言data屬性是一個(gè)對(duì)象
expect(res.body.data).to.be.an('object');
done();
});
})
it('registerUser', (done) => {
// 請(qǐng)求參數(shù)卖氨,模擬用戶對(duì)象
var user = {
username: '阿,希爸',
age: 31
}
request(app.listen())
.post('/api/users/registerUser') //post方法
.send(user) //添加請(qǐng)求參數(shù)
.set('Content-Type', 'application/json') //設(shè)置header的Content-Type為json
.expect(200) //斷言狀態(tài)碼為200
.end((err, res) => {
console.log(res.body);
//斷言返回的code是0
expect(res.body.code).to.be.equal(0);
done();
})
})
})
如果現(xiàn)在直接運(yùn)行npm test
進(jìn)行測(cè)試會(huì)報(bào)錯(cuò)负懦,原因是mocha默認(rèn)是不支持async await
語法筒捺,解決的辦法是Babel。
Babel的主要作用是對(duì)不同版本的js進(jìn)行轉(zhuǎn)碼纸厉。
如果你對(duì)Babel不了解系吭,請(qǐng)仔細(xì)閱讀Babel 入門教程與Babel官網(wǎng)。
由于koa-generator已經(jīng)幫我們添加相關(guān)的Babel依賴颗品,我們只需要添加相關(guān)的規(guī)則就可以了肯尺。在項(xiàng)目的根目錄下添加一個(gè).babelrc文件,內(nèi)容如下:
{
"env": {
"test": {
"presets": ["es2015-node5"],
"plugins": [
"transform-async-to-generator",
"syntax-async-functions"
]
}
}
}
這段文件的意思是對(duì)當(dāng)env=test時(shí)躯枢,應(yīng)用es2015-node5
则吟、transform-async-to-generator
、syntax-async-functions
規(guī)則進(jìn)行轉(zhuǎn)碼锄蹂。
Babel我們?cè)O(shè)置好了氓仲,想要mocha應(yīng)用這個(gè)規(guī)則還要在執(zhí)行時(shí)添加一個(gè)命令。
打開package.json,將scripts.test修改為:
"test": "NODE_ENV=test mocha --compilers js:babel-core/register",
在終端執(zhí)行npm test
敬扛,輸出如下內(nèi)容說明測(cè)試通過晰洒。
user_api
<-- GET /api/users/getUser?id=1
--> GET /api/users/getUser?id=1 200 14ms 74b
{ code: 0,
message: 'success',
data: { username: '阿,希爸', age: 30 } }
? getUser (57ms)
<-- POST /api/users/registerUser
registerUser { username: '阿啥箭,希爸', age: 31 }
--> POST /api/users/registerUser 200 2ms 30b
{ code: 0, message: 'success' }
? registerUser