实现一个简单的Koa

作者: 唔六 | 来源:发表于2019-03-16 10:31 被阅读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协议主要经历了这样三个阶段:

    • 1.0 只允许一个时间内只能接受一个请求。(allowed one request to be outstanding aet a time)
    • 1.1 使用 pipeline的方式来处理请求,但是仍然会出现 排头堵塞(head-of-line blocking)。
    • 2 增加了长连接……

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

    HTTP/2 RFC

    服务器应该具备的功能

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

    其核心要素三点:

    • 监听客户端请求
    • 处理客户端请求
    • 响应客户端请求

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

    koa的监听、处理、响应

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

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

    • 从响应的状态上来看:
    常用的Http <wbr>Response <wbr>Code状态码一览表

    ……响应类型大全

    • 从响应的类型上看
      • 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()
                });
        });
    });
    
    

    参考 & 引用

    从零实现一个http服务器

    相关文章

      网友评论

        本文标题:实现一个简单的Koa

        本文链接:https://www.haomeiwen.com/subject/cmpjmqtx.html