從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
得到:
思考什么是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)。
服務(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)看:
- 從響應(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()
});
});
});