《koa誕生記》——實(shí)現(xiàn)一個(gè)簡(jiǎn)單的Koa

從http.createServer開始

先用最簡(jiǎn)單的方法來(lái)實(shí)現(xiàn)一個(gè)web服務(wù)器褒脯,命名為koa_01.js

let app = http.createServer( (req, res) => {
  let body = [];
  res.writeHead(200, {
    'content-type': 'text-html'
  });
  res.write('');
  res.end('First test')
});

module.exports = app;
if (!moudle.parent) app.listen(3000);

在瀏覽器中進(jìn)行測(cè)試并不是一個(gè)好做法覆致。我們可以使用 mocha + supertest來(lái)驗(yàn)證我們的服務(wù)器是否創(chuàng)建成功疗隶。

為了保持跟Koa的原始實(shí)現(xiàn)保持一致,包的版本如下:

  • "should": "^13.2.3"
  • "supertest": "^4.0.0"
  • "co":"1.5.1"

將測(cè)試文件命名為 first_koa.test.js

let app = require('./koa_01.js')
let request = require('supertest').agent;
require('should');

describe('第一個(gè)測(cè)試:簡(jiǎn)單服務(wù)器', () => {
    it('返回狀態(tài)應(yīng)為200', (done) => {
        let server = app.listen(8900)
        koa_request(server)
            .get('/')
            .expect(200)
            .end((err, res) => {
                done()
            })
    });
});

運(yùn)行 mocha first_koa.test.js 得到:

測(cè)試結(jié)果

思考什么是HTTP Server

http協(xié)議

目前已經(jīng)有很多服務(wù)器支持 HTTP/2,簡(jiǎn)單的來(lái)說(shuō),http協(xié)議主要經(jīng)歷了這樣三個(gè)階段:

  • 1.0 只允許一個(gè)時(shí)間內(nèi)只能接受一個(gè)請(qǐng)求焊唬。(allowed one request to be outstanding aet a time)
  • 1.1 使用 pipeline的方式來(lái)處理請(qǐng)求,但是仍然會(huì)出現(xiàn) 排頭堵塞(head-of-line blocking)看靠。
  • 2 增加了長(zhǎng)連接……

協(xié)議更加詳細(xì)的介紹赶促,建議大家可以看最新的標(biāo)準(zhǔn)。

HTTP/2 RFC

服務(wù)器應(yīng)該具備的功能

無(wú)論web服務(wù)器現(xiàn)在如何發(fā)展挟炬,能夠?qū)崿F(xiàn)正確進(jìn)行http協(xié)議交互的服務(wù)端鸥滨,我們都可以稱之為web服務(wù)器。

其核心要素三點(diǎn):

  • 監(jiān)聽客戶端請(qǐng)求
  • 處理客戶端請(qǐng)求
  • 響應(yīng)客戶端請(qǐng)求

其它的諸如對(duì)性能谤祖、安全婿滓、日志等等方面的實(shí)現(xiàn),甚至于對(duì)各類語(yǔ)言的支持泊脐,雖然也很重要空幻,但并不是web服務(wù)器最核心的理念烁峭。

koa的監(jiān)聽容客、處理、響應(yīng)

監(jiān)聽http請(qǐng)求约郁,可以通過(guò)nodejs自帶的 API進(jìn)行實(shí)現(xiàn)缩挑。koa主要關(guān)注如何處理及響應(yīng)請(qǐng)求。

實(shí)際上對(duì)請(qǐng)求的處理和響應(yīng)可以放到一塊鬓梅。在 restful標(biāo)準(zhǔn)中供置,請(qǐng)求只是定義一個(gè) 名詞描述 (url) 和 動(dòng)詞方法 (get,post,put,delete)。

  • 從響應(yīng)的狀態(tài)上來(lái)看:
常用的Http <wbr>Response <wbr>Code狀態(tài)碼一覽表

……響應(yīng)類型大全

  • 從響應(yīng)的類型上看
    • String
    • Buffer
    • Stream
    • Object

所以绽快,如果不考慮其它情況芥丧,我們實(shí)現(xiàn)一個(gè)對(duì)請(qǐng)求處理的函數(shù)大概如下:

function respond() {
    var res = this.res;
    var body = this.body;
    var head = 'HEAD' == this.method;
    var ignore = 204 == this.status || 304 == this.status;

    // 404
    if (null == body && 200 == this.status) {
      this.status = 404;
    }

    // body為空
    if (ignore) return res.end();

    // ignore情況
    if (null == body) {
      this.set('Content-Type', 'text/plain');
      body = http.STATUS_CODES[this.status];
    }
    
    // Buffer body
    if (Buffer.isBuffer(body)) {
      var ct = this.responseHeader['content-type'];
      if (!ct) this.set('Content-Type', 'application/octet-stream');
      this.set('Content-Length', body.length);
      if (head) return res.end();
      return res.end(body);
    }

    // string body
    if ('string' == typeof body) {
      var ct = this.responseHeader['content-type'];
      if (!ct) this.set('Content-Type', 'text/plain; charset=utf-8');
      this.set('Content-Length', Buffer.byteLength(body));
      if (head) return res.end();
      return res.end(body);
    }

    // Stream body
    if (body instanceof Stream) {
      body.on('error', this.error.bind(this));
      if (head) return res.end();
      return body.pipe(res);
    }
    
    // body: json
    body = JSON.stringify(body, null, this.app.jsonSpaces);
    this.set('Content-Length', body.length);
    this.set('Content-Type', 'application/json');
    if (head) return res.end();
    res.end(body);
  }
}

那么紧阔,將這個(gè)函數(shù)封裝到第一步中的簡(jiǎn)單服務(wù)器請(qǐng)求中,應(yīng)該是這樣的:

koa_02.js

let app = http.createServer( (req, res) => {
  let body = 'test';
  let context = {req, res, body}
  respond.call(context)
});

module.exports = app;
if (!moudle.parent) app.listen(3001);

koa之中間件

上面第二個(gè)版本的實(shí)現(xiàn)续担∩玫ⅲ基本完成一個(gè)web服務(wù)的框架。那么有以下幾個(gè)問(wèn)題:

  • 響應(yīng)的body應(yīng)該如何設(shè)置
  • 如何更改響應(yīng)頭
  • 如何增加路由物遇、日志等等功能

……

這些問(wèn)題乖仇,實(shí)際上有很多的實(shí)現(xiàn)方法。koa主要采用 洋蔥模型询兴。更加詳細(xì)的介紹可以參看 koa中間件

  • 中間件函數(shù)fn需要push到數(shù)組中乃沙。

    middlware.push(fn)

  • 調(diào)用的時(shí)候,需要將狀態(tài)(request, response)保存到一個(gè)對(duì)象中.

    let ctx = Context(self, req, res)

  • 所有的函數(shù)都需要放在http服務(wù)的回調(diào)函數(shù)中

    let server = http.createServer(this.callback())

  • 請(qǐng)求需要經(jīng)過(guò)所有的中間件诗舰。最后一定是respond函數(shù)警儒,否則處理到最后,就無(wú)法完成響應(yīng)的過(guò)程眶根。

    [respond].concat(middleware)

具體實(shí)現(xiàn)

考慮到上述要求冷蚂,定義一個(gè)對(duì)象Application。

Koa_03.js

function Application() {
    if (!(this instanceof Application)) return new Application;
    this.env = process.env.NODE_ENV || 'development';
    this.outputErrors = 'development' == this.env;
    this.middleware = [];
}
  • 對(duì)象應(yīng)該能夠?qū)崿F(xiàn)http服務(wù)
app = Applicatin.prototype
app.listen = function () {
    let server = http.createServer(this.callback());
    return server.listen.apply(server, arguments);
}
  • 對(duì)象應(yīng)該允許push中間件函數(shù)
app.use = function(fn) {
    // debug('use %s ', fn.name || 'unnamed');
    this.middleware.push(fn);
    return this;
}
  • 封裝的callback可以按照規(guī)則執(zhí)行中間件
app.callback = function () {
    // 首先push進(jìn)respond函數(shù)
    let mw = [respond].concat(this.middleware);
    let fn = compose(mw)(downstream);
    let self = this;
    return function (req, res) {
        // let ctx = new Context(self, req, res);
        let ctx = new Context(self, req, res);

        function done (err) {
            // if (err) ctx.error(err);
            // console.log(err)
            if(err) throw new Error('sdf')
        }
        
        co.call(ctx, function *() {
            yield fn;
        }, done);
    }
};
  • 保證respond函數(shù)能夠最后執(zhí)行
function respond(next) {
    return function *() {
        yield next;
        st = this.status
        let res = this.res;
        let body = this.body;
        let head = 'HEAD' == this.method;
        let ignore = 204 == this.status || 304 == this.status;

        this.status = 200;
        if (null == body && 200 == this.status) {
            this.status = 404;
        }

        if (ignore) return res.end();
        if (null == body) {
            this.set('Content-Type', 'text/plain');
            body = http.STATUS_CODES[this.status];
        }

        if (Buffer.isBuffer(body)) {
            
        }
        res.write('')
        res.end('');

    }
}
  • Context應(yīng)該保存所有的狀態(tài)
function Context(app, req, res) {
    this.app = app;
    this.req = req;
    this.res = res;
}
……

測(cè)試

koa_03.test.js

let request = require('supertest').agent;
const koa = require('../koa_03.js');
const app = new koa()
const http = require('http');
require('should');


describe('koa 03正常啟動(dòng)web服務(wù)', () => {
    it('響應(yīng)為200', (done) => {
        let server = app.listen(8900)
        request(server)
            .get('/')
            .expect(200)
            .end((err, res) => {
                done()
            })
    });
});


describe('koa可以執(zhí)行一個(gè)中間件函數(shù)', () => {
    it('reponse with 200', (done) => {
        let calls = [];
        app.use(function(next) {
            return function * () {
                calls.push(1);
                yield next;
            }
        });
        let server = app.listen(8901)
        request(server)
            .get('/')
            .expect(200)
            .end((err, res) => {
                calls.should.eql([1])
                done()
            })
    });
})


describe('運(yùn)行中間件函數(shù)流程正確', () => {
    it('執(zhí)行流程應(yīng)為 1汛闸,2蝙茶,3,4诸老,5隆夯,6,響應(yīng)請(qǐng)求', (done) => {
        let app = new koa();
        let calls = [];
        app.use(function(next) {
            return function * () {
                calls.push(1);
                yield next;
                calls.push(6);
            }
        });

        app.use(function(next) {
            return function * () {
                calls.push(2);
                yield next;
                calls.push(5);
            }
        });

        app.use(function(next) {
            return function * () {
                calls.push(3);
                yield next;
                calls.push(4);
            }
        }); 

        server = app.listen(9000);
        request(server)
            .get('/')
            .end(function(err) {
                calls.should.eql([1,2,3,4,5,6])
                if (err) return done(err);
                done()
            });
    });
});

參考 & 引用

從零實(shí)現(xiàn)一個(gè)http服務(wù)器

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末别伏,一起剝皮案震驚了整個(gè)濱河市蹄衷,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌厘肮,老刑警劉巖愧口,帶你破解...
    沈念sama閱讀 216,496評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異类茂,居然都是意外死亡义图,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門崖面,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)椅寺,“玉大人,你說(shuō)我怎么就攤上這事兢哭×旖ⅲ” “怎么了?”我有些...
    開封第一講書人閱讀 162,632評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)冲秽。 經(jīng)常有香客問(wèn)我舍咖,道長(zhǎng),這世上最難降的妖魔是什么锉桑? 我笑而不...
    開封第一講書人閱讀 58,180評(píng)論 1 292
  • 正文 為了忘掉前任谎仲,我火速辦了婚禮,結(jié)果婚禮上刨仑,老公的妹妹穿的比我還像新娘郑诺。我一直安慰自己,他們只是感情好杉武,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,198評(píng)論 6 388
  • 文/花漫 我一把揭開白布辙诞。 她就那樣靜靜地躺著,像睡著了一般轻抱。 火紅的嫁衣襯著肌膚如雪飞涂。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,165評(píng)論 1 299
  • 那天祈搜,我揣著相機(jī)與錄音较店,去河邊找鬼。 笑死容燕,一個(gè)胖子當(dāng)著我的面吹牛梁呈,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播蘸秘,決...
    沈念sama閱讀 40,052評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼官卡,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了醋虏?” 一聲冷哼從身側(cè)響起寻咒,我...
    開封第一講書人閱讀 38,910評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎颈嚼,沒(méi)想到半個(gè)月后毛秘,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,324評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡阻课,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,542評(píng)論 2 332
  • 正文 我和宋清朗相戀三年叫挟,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片柑肴。...
    茶點(diǎn)故事閱讀 39,711評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡霞揉,死狀恐怖旬薯,靈堂內(nèi)的尸體忽然破棺而出晰骑,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 35,424評(píng)論 5 343
  • 正文 年R本政府宣布硕舆,位于F島的核電站秽荞,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏抚官。R本人自食惡果不足惜扬跋,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,017評(píng)論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望凌节。 院中可真熱鬧钦听,春花似錦、人聲如沸倍奢。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)卒煞。三九已至痪宰,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間畔裕,已是汗流浹背衣撬。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留扮饶,地道東北人具练。 一個(gè)月前我還...
    沈念sama閱讀 47,722評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像甜无,于是被迫代替她去往敵國(guó)和親靠粪。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,611評(píng)論 2 353