码农的世界让前端飞程序员

实现一个简单的Koa

2019-03-16  本文已影响5人  唔六

从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协议主要经历了这样三个阶段:

协议更加详细的介绍,建议大家可以看最新的标准。

HTTP/2 RFC

服务器应该具备的功能

无论web服务器现在如何发展,能够实现正确进行http协议交互的服务端,我们都可以称之为web服务器。

其核心要素三点:

其它的诸如对性能、安全、日志等等方面的实现,甚至于对各类语言的支持,虽然也很重要,但并不是web服务器最核心的理念。

koa的监听、处理、响应

监听http请求,可以通过nodejs自带的 API进行实现。koa主要关注如何处理及响应请求。

实际上对请求的处理和响应可以放到一块。在 restful标准中,请求只是定义一个 名词描述 (url) 和 动词方法 (get,post,put,delete)。

常用的Http <wbr>Response <wbr>Code状态码一览表

……响应类型大全

所以,如果不考虑其它情况,我们实现一个对请求处理的函数大概如下:

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服务的框架。那么有以下几个问题:

……

这些问题,实际上有很多的实现方法。koa主要采用 洋葱模型。更加详细的介绍可以参看 koa中间件

具体实现

考虑到上述要求,定义一个对象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 = [];
}
app = Applicatin.prototype
app.listen = function () {
    let server = http.createServer(this.callback());
    return server.listen.apply(server, arguments);
}
app.use = function(fn) {
    // debug('use %s ', fn.name || 'unnamed');
    this.middleware.push(fn);
    return this;
}
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);
    }
};
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('');

    }
}
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()
            });
    });
});

参考 & 引用

从零实现一个http服务器

上一篇下一篇

猜你喜欢

热点阅读