「新手向」koa2從起步到填坑

前傳

出于興趣最近開始研究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)行成功理盆。


在此再次感謝狼叔-桑世龍

當(dāng)前項(xiàng)目的文件目錄如下圖


FileTree

1.4 關(guān)于koa2

1.4.1 中間件的執(zhí)行順序

koa的中間件是由generator組成的凑阶,這決定了中間件的執(zhí)行順序猿规。
Express的中間件是順序執(zhí)行,從第一個(gè)中間件執(zhí)行到最后一個(gè)中間件宙橱,發(fā)出響應(yīng)姨俩。

koa是從第一個(gè)中間件開始執(zhí)行,遇到<code>next</code>進(jìn)入下一個(gè)中間件师郑,一直執(zhí)行到最后一個(gè)中間件环葵,在逆序,執(zhí)行上一個(gè)中間件<code>next</code>之后的代碼宝冕,一直到第一個(gè)中間件執(zhí)行結(jié)束才發(fā)出響應(yīng)积担。

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ù)庫(kù),測(cè)試要使用測(cè)試庫(kù)襟雷,發(fā)布上線時(shí)候使用線上的庫(kù)刃滓,也會(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ù)庫(kù)地址
    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ù)庫(kù)地址
    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)生了日子文件申尤。

內(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文件。


代碼如下:

//獲取用戶
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提供的斷言庫(kù)。

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è)斷言庫(kù)。之前的例子中灭红,我們使用的是node提供的斷言庫(kù)侣滩,他的功能比較少,基本上只有equal变擒、ok君珠、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

有關(guān)supertest的更多用法請(qǐng)參考 github_supertest

如需查看項(xiàng)目代碼 –> 代碼地址:

https://github.com/tough1985/hello-koa2
選擇Tag -> step5

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末刃麸,一起剝皮案震驚了整個(gè)濱河市醒叁,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌泊业,老刑警劉巖把沼,帶你破解...
    沈念sama閱讀 219,039評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異吁伺,居然都是意外死亡智政,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門箱蝠,熙熙樓的掌柜王于貴愁眉苦臉地迎上來续捂,“玉大人,你說我怎么就攤上這事宦搬⊙榔埃” “怎么了?”我有些...
    開封第一講書人閱讀 165,417評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵间校,是天一觀的道長(zhǎng)矾克。 經(jīng)常有香客問我,道長(zhǎng)憔足,這世上最難降的妖魔是什么胁附? 我笑而不...
    開封第一講書人閱讀 58,868評(píng)論 1 295
  • 正文 為了忘掉前任酒繁,我火速辦了婚禮,結(jié)果婚禮上控妻,老公的妹妹穿的比我還像新娘州袒。我一直安慰自己,他們只是感情好弓候,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,892評(píng)論 6 392
  • 文/花漫 我一把揭開白布郎哭。 她就那樣靜靜地躺著,像睡著了一般菇存。 火紅的嫁衣襯著肌膚如雪夸研。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,692評(píng)論 1 305
  • 那天依鸥,我揣著相機(jī)與錄音亥至,去河邊找鬼。 笑死贱迟,一個(gè)胖子當(dāng)著我的面吹牛抬闯,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播关筒,決...
    沈念sama閱讀 40,416評(píng)論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼溶握,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了蒸播?” 一聲冷哼從身側(cè)響起睡榆,我...
    開封第一講書人閱讀 39,326評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎袍榆,沒想到半個(gè)月后胀屿,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,782評(píng)論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡包雀,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,957評(píng)論 3 337
  • 正文 我和宋清朗相戀三年宿崭,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片才写。...
    茶點(diǎn)故事閱讀 40,102評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡葡兑,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出赞草,到底是詐尸還是另有隱情讹堤,我是刑警寧澤,帶...
    沈念sama閱讀 35,790評(píng)論 5 346
  • 正文 年R本政府宣布厨疙,位于F島的核電站洲守,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜梗醇,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,442評(píng)論 3 331
  • 文/蒙蒙 一知允、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧叙谨,春花似錦温鸽、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽配椭。三九已至虫溜,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間股缸,已是汗流浹背衡楞。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留敦姻,地道東北人瘾境。 一個(gè)月前我還...
    沈念sama閱讀 48,332評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像镰惦,于是被迫代替她去往敵國(guó)和親迷守。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,044評(píng)論 2 355

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