从http.createServer开始
先用最简单的方法来实现一个web服务器,命名为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);
在浏览器中进行测试并不是一个好做法。我们可以使用 mocha
+ supertest
来验证我们的服务器是否创建成功。
为了保持跟Koa的原始实现保持一致,包的版本如下:
- "should": "^13.2.3"
- "supertest": "^4.0.0"
- "co":"1.5.1"
将测试文件命名为 first_koa.test.js
let app = require('./koa_01.js')
let request = require('supertest').agent;
require('should');
describe('第一个测试:简单服务器', () => {
it('返回状态应为200', (done) => {
let server = app.listen(8900)
koa_request(server)
.get('/')
.expect(200)
.end((err, res) => {
done()
})
});
});
运行 mocha first_koa.test.js
得到:
思考什么是HTTP Server
http协议
目前已经有很多服务器支持 HTTP/2
,简单的来说,http协议主要经历了这样三个阶段:
- 1.0 只允许一个时间内只能接受一个请求。(allowed one request to be outstanding aet a time)
- 1.1 使用
pipeline
的方式来处理请求,但是仍然会出现排头堵塞
(head-of-line blocking)。 - 2 增加了长连接……
协议更加详细的介绍,建议大家可以看最新的标准。
服务器应该具备的功能
无论web服务器现在如何发展,能够实现正确进行http协议交互的服务端,我们都可以称之为web服务器。
其核心要素三点:
- 监听客户端请求
- 处理客户端请求
- 响应客户端请求
其它的诸如对性能、安全、日志等等方面的实现,甚至于对各类语言的支持,虽然也很重要,但并不是web服务器最核心的理念。
koa的监听、处理、响应
监听http请求,可以通过nodejs
自带的 API
进行实现。koa主要关注如何处理及响应请求。
实际上对请求的处理和响应可以放到一块。在 restful
标准中,请求只是定义一个 名词描述
(url) 和 动词方法
(get,post,put,delete)。
- 从响应的状态上来看:
……响应类型大全
- 从响应的类型上看
- String
- Buffer
- Stream
- Object
所以,如果不考虑其它情况,我们实现一个对请求处理的函数大概如下:
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);
}
}
那么,将这个函数封装到第一步中的简单服务器请求中,应该是这样的:
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之中间件
上面第二个版本的实现。基本完成一个web服务的框架。那么有以下几个问题:
- 响应的body应该如何设置
- 如何更改响应头
- 如何增加路由、日志等等功能
……
这些问题,实际上有很多的实现方法。koa主要采用 洋葱模型
。更加详细的介绍可以参看 koa中间件
-
中间件函数
fn
需要push到数组中。middlware.push(fn)
-
调用的时候,需要将状态(request, response)保存到一个对象中.
let ctx = Context(self, req, res)
-
所有的函数都需要放在http服务的回调函数中
let server = http.createServer(this.callback())
-
请求需要经过所有的中间件。最后一定是respond函数,否则处理到最后,就无法完成响应的过程。
[respond].concat(middleware)
具体实现
考虑到上述要求,定义一个对象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 = [];
}
- 对象应该能够实现http服务
app = Applicatin.prototype
app.listen = function () {
let server = http.createServer(this.callback());
return server.listen.apply(server, arguments);
}
- 对象应该允许push中间件函数
app.use = function(fn) {
// debug('use %s ', fn.name || 'unnamed');
this.middleware.push(fn);
return this;
}
- 封装的callback可以按照规则执行中间件
app.callback = function () {
// 首先push进respond函数
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函数能够最后执行
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应该保存所有的状态
function Context(app, req, res) {
this.app = app;
this.req = req;
this.res = res;
}
……
测试
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正常启动web服务', () => {
it('响应为200', (done) => {
let server = app.listen(8900)
request(server)
.get('/')
.expect(200)
.end((err, res) => {
done()
})
});
});
describe('koa可以执行一个中间件函数', () => {
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('运行中间件函数流程正确', () => {
it('执行流程应为 1,2,3,4,5,6,响应请求', (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()
});
});
});
网友评论