koa2 項(xiàng)目基本構(gòu)建(參考)

前傳

出于興趣最近開始研究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.jsonscripts對(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)境配置好喻杈,例如我們將starttest分別設(shè)置developmenttest環(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-loggertj大神寫的koa開發(fā)時(shí)替換console.log輸出的一個(gè)插件爆侣。

如果你需要按照時(shí)間或者按照文件大小萍程,本地輸出log文件的話,建議還是采用log4js-node兔仰。

3.1 log4js

log4js提供了多個(gè)日志等級(jí)分類茫负,同時(shí)也能替換console.log輸出,另外他還可以按照文件大小或者日期來生成本地日志文件乎赴,還可以使用郵件等形式發(fā)送日志忍法。

我們?cè)谶@演示用infoerror兩種日志等級(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;

這樣就完成了getUserregisterUser進(jìn)行了路由配置么翰,其中getUserGET方式請(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);
        }
    }
}

解釋一下這段代碼

  1. 使用<code>try catch</code>包裹<code>await next();</code>软啼,這樣后面的中間件拋出的異常都可以在這幾集中處理;

  2. <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è)試框架基本就是mochaAVA了增热,這里主要以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提供的斷言庫柳骄,他的功能比較少团赏,基本上只有equalok耐薯、fail這樣簡(jiǎn)單的功能舔清,很難滿足日常的需求。

mocha官方表示你愛用什么斷言用什么斷言曲初,反正老子都支持体谒。

選擇chai是因?yàn)樗麑?duì)斷言的幾種語法都支持,而且功能也比較全面 --> chai官網(wǎng)臼婆。

chai支持should抒痒、expectassert三種斷言形式。

assert語法之前我們已經(jīng)見過了目锭,chai只是豐富了功能评汰,語法并沒有變化。
expectshould的語法更接近自然語言的習(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)單的解釋其中的tobe這樣的語法。

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接口getUserregisterUser進(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-generatorsyntax-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
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末谍珊,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子急侥,更是在濱河造成了極大的恐慌砌滞,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,383評(píng)論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件缆巧,死亡現(xiàn)場(chǎng)離奇詭異布持,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)陕悬,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,522評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門题暖,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人捉超,你說我怎么就攤上這事胧卤。” “怎么了拼岳?”我有些...
    開封第一講書人閱讀 157,852評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵枝誊,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我惜纸,道長(zhǎng)叶撒,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,621評(píng)論 1 284
  • 正文 為了忘掉前任耐版,我火速辦了婚禮祠够,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘粪牲。我一直安慰自己古瓤,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,741評(píng)論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般劣光。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上绎速,一...
    開封第一講書人閱讀 49,929評(píng)論 1 290
  • 那天,我揣著相機(jī)與錄音痛侍,去河邊找鬼朝氓。 笑死魔市,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的赵哲。 我是一名探鬼主播待德,決...
    沈念sama閱讀 39,076評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼枫夺!你這毒婦竟也來了将宪?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,803評(píng)論 0 268
  • 序言:老撾萬榮一對(duì)情侶失蹤橡庞,失蹤者是張志新(化名)和其女友劉穎较坛,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體扒最,經(jīng)...
    沈念sama閱讀 44,265評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡丑勤,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,582評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了吧趣。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片法竞。...
    茶點(diǎn)故事閱讀 38,716評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖强挫,靈堂內(nèi)的尸體忽然破棺而出岔霸,到底是詐尸還是另有隱情,我是刑警寧澤俯渤,帶...
    沈念sama閱讀 34,395評(píng)論 4 333
  • 正文 年R本政府宣布呆细,位于F島的核電站,受9級(jí)特大地震影響八匠,放射性物質(zhì)發(fā)生泄漏絮爷。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,039評(píng)論 3 316
  • 文/蒙蒙 一梨树、第九天 我趴在偏房一處隱蔽的房頂上張望略水。 院中可真熱鬧,春花似錦劝萤、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,798評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至胸私,卻和暖如春厌处,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背岁疼。 一陣腳步聲響...
    開封第一講書人閱讀 32,027評(píng)論 1 266
  • 我被黑心中介騙來泰國打工阔涉, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留缆娃,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,488評(píng)論 2 361
  • 正文 我出身青樓瑰排,卻偏偏與公主長(zhǎng)得像贯要,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子椭住,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,612評(píng)論 2 350

推薦閱讀更多精彩內(nèi)容