美文网首页
Koa2源码分析

Koa2源码分析

作者: 宫若石 | 来源:发表于2017-11-30 19:49 被阅读0次

    源码结构

    Koa的源码中主要为lib目录下的application.js, context.js, request.js与response.js文件

    .
    ├── AUTHORS
    ├── CODE_OF_CONDUCT.md
    ├── History.md
    ├── LICENSE
    ├── Makefile
    ├── Readme.md
    ├── benchmarks
    ├── docs
    ├── lib
    │   ├── application.js
    │   ├── context.js
    │   ├── request.js
    │   └── response.js
    ├── package.json
    └── test
    

    application.js: 框架入口,导出Application类,即使用时倒入的Koa类。
    context.js: context对象的原型,代理request与response对象。
    request.js: request对象的原型,提供请求相关的数据与操作。
    response,js: response对象的原型,提供响应相关的数据与操作。

    Application

    • proxy: 是否信任proxy header参数,默认为false
    • middleware: 保存通过app.use(middleware)注册的中间件
    • subdomainOffset: 保存通过app.use(middleware)注册的中间件
    • env: 环境参数,默认为NODE_ENV或'development'
    • context: context模块,通过context.js创建
    • request: request模块,通过request.js创建
    • response: response模块,通过response.js创建

    Application@listen

    Koa通过app.listen(port)函数在某个端口启动服务。
    listen函数通过http模块开启服务:

    /**
     * shorthand for:
     *  http.createServer(app.callback()).listen(...)
     *
     * @param {Mixed} ...
     * @return {Server}
     * @api public
     */
    listen(...args) {
      debug('listen');
      const server = http.createServer(this.callback());
      return server.listen(...args);
    }
    

    实际上app.listen()为http.createServer(app.callback()).listen(...)的速记写法。http.createServer()用于创建Web服务器,接受一个请求监听函数,并在得到请求时执行。app.callback()用于处理请求,合并中间件与创建请求上下文对象等等。

    Application@use

    Koa通过app.use()添加中间件,并将中间件存储在app.middleware中。在执行app.callback()时会将app,middleware中的中间件合并为一个函数。

    /**
     * Use the given middleware 'fn',
     *
     * Old-style middleware will be converted.
     *
     * @param {Function} fn
     * @return {Application} self
     * @api public
     */
    use(fn) {
      if(typeof fn !== 'function') throw new TypeError('middleware must be a function!');
      if(isGeneratorFunction(fn)) {
        deprecate('Support for generators will be removed in v3. ' + 
                  'See the documentation for examples of how to convert old middleware ' + 
                  'https://github.com/koajs/koa/blob/master/docs/migration.md');
        fn = convert(fn);
      }
      debug('use %s', fn._name || fn.name || '-');
      this.middleware.push(fn);
      return this;
    }
    

    Koa1.x版本使用Generator Function的方式写中间件,而Koa2改用ES6 async/await。所以在use()函数中会判断是否为旧风格的中间件写法,并对旧风格写法得中间件进行转换(使用koa-convert进行转换)。
    可以注意到这里use()函数返回了this,这使得在添加中间件的时候能链式调用。

    app
      .use(function(ctx, next) {
        //  do some thing
      })
      .use(function(ctx, next) {
        //  do some thing
      })
      // ...
    

    Application@callback

    app.callback()负责合并中间件,创建请求上下文对象以及返回请求处理函数等。

    /**
     * Return a request handler callback
     * for node's native http server
     *
     * @return {Function}
     * @api public
     */
    
    callback() {
      const fn = compose(this.middleware);
    
      if (!this.listeners('error').length) this.on('error', this.onerror);
    
      const handleRequest = (req, res) => {
        res.statusCode = 404;
        const ctx = this.createContext(req, res);
        const onerror = err => ctx.onerror(err);
        const handlerResponse = () => respond(ctx);
        onFinished(res, onerror);
        return fn(ctx).then(handleResponse.catch(onerror));
      };
    
      return handleRequest;
    }
    

    通过compose函数(koa-compose)合并app.middleware中的所有中间件。查看关于koa-compose的分析。
    app.callback()函数返回一个请求处理函数handleRequest。该函数即为http.createServer接收的请求处理函数,在得到请求时执行。

    handleRequest

    handleRequest函数首先将响应状态设置为404,接着通过app.createContext()创建请求的上下文对象。
    onFinished(res, onerror)通过第三方库on-finished监听http response,当请求结束时执行回调,这里传入的回调是context.onerror(err),即当错误发生时才执行。
    最后返回fn(ctx).then(handleResponse).catch(onerror),即将所有中间件执行(传入请求上下文对象ctx),之后执行响应处理函数(app.respond(ctx)),当抛出异常时同样使用cintext,onerror(err)处理。

    createContext

    app.createContext()用来创建请求上下文对象,并代理Koa的request和response模块。

    /**
     * Initialize a new context
     *
     * @api private
     */
    
    createContext(req, res) {
      const context = Object.create(this.context);
      const request = context.request = Object.create(this.response);
      const response = context.response = Object.create(this.response);
      context.app = request.app = response.app = this;
      context.req = request.req = response.req = req;
      context.res = request.res = response.res = res;
      request.response = response;
      response.request = request;
      context.originalUrl = request.originalUrl = req.url;
      context.cookies = new Cookies(req, res, {
        keys: this.keys,
        secure: request.secure
      });
      request.ip = request.ips[0] || req.socket.remoteAddress || '';
      context.accept = request.accept = accepts(req); 
      context.state = {};
      return context;
    }
    

    这里对请求都对应在上下文对象中添加对应的cookies。

    respond

    app.respond(ctx)函数,这就是app.createContext()函数中的handleResponse,在所有中间件执行完之后执行。
    在koa中可以通过设置ctx.respond = false来跳过这个函数,但不推荐这样子。另外,当上下文对象不可写时也会退出该函数:

    if (false === ctx.respond) return;
    // ...
    if (!ctx.writable) return;
    

    当返回的状态码表示没有响应主体时,将响应主体置空:

    // ignore body
    if (statues.empty[code]) {
      // strip headers
      ctx.body = null;
      return res.end();
    }
    

    当请求方法为HEAD时,判断响应头是否发送以及响应主体是否为JSON格式,若满足则设置响应Content-Length:

    if('HEAD' == ctx.method) {
      if(!res.headersSent && isJSON(body)) {
        ctx.length = Buffer.byteLength(JSON.stringify(body));
      }
      return res.end();
    }
    

    当返回的状态码表示有响应主体,但响应主体为空时,将响应主体设置为响应信息或状态码。并当响应头未发送时设置Content-Type与Content-Length:

    if (null == body) {
      body = ctx.message || String(code);
      if (!res.headersSent) {
        ctx.type = 'text';
        ctx.length = Buffer.byteLength(body);
      }
      return res.end(body);
    }
    

    最后,对不同的响应主体进行处理:

    // response
    if (Buffer.isBuffer(body)) return res.end(body);
    if ('string' == typeof body) return res.end(body);
    if(body instanceof Stream) return body.pipe(res);
    
    // body: json
    body = JSON.stringify(body);
    if(!res.headersSent) {
      ctx.length = Buffer.byteLength(body);
    }
    
    res.end(body);
    

    Compose

    在application.js中,callback()函数通过koa-compose组合所有的中间件,组合成单个函数。koa-compose的实现很简单:

    function compose (middleware) {
      if (!Array.isArray(middleware)) throw new TypeError('middleware stack must be an array!');
      for(const fn of middleware) {
        if (typeof fn !== 'function') throw new TypeError('middleware must be composed of functions!');
      }
    
      return function (context, next) {
        // last called middleware #
        let index = -1;
        return dispatch(0);
        function dispatch(i) {
          if (i <= index) return Promise.reject(new Error('next() called multiple times'));
          index = i;
          let fn = middleware[i];
          if (i === middleware.length) fn = next;
          if (!fn) return Promise.resolve();
          try {
            return Promise.resolve(fn(context, function next() {
              return dispatch(i + 1);
            }));
          } catch (err) {
            return Promise.reject(err);
          }
        }
      }
    }
    

    首先判断传入的中间件参数是否为数组,并检查且该数组的元素是否为函数,然后返回了一个将中间件组合起来的函数。
    重点关注返回的函数中的dispatch(i)函数,这个函数将获取第一个中间件,并在返回的Promise中执行。当中间件await next()时执行下一个中间件,即:dispatch(i+1)。
    执行流程可以简单看做:

    async function middleware1() {
      console.log('middleware1 begin');
      await middleware2();
      console.log('middleware1 end');
    }
    
    async function middleware2() {
      console.log('middleware2 begin');
      await middleware3();
      console.log('middleware2 end');
    }
    
    async function middleware3() {
      console.log('middleware3 begin');
      console.log('middleware3 end');
    }
    
    middleware1();
    //  执行结果
    middleware1 begin
    middleware2 begin
    middleware3 begin
    middleware3 end
    middleware2 end
    middleware1 end
    

    compose()函数通过Promise将这个过程串联起来,从而返回单个中间件函数。

    Context

    Koa中的Context模块封装了request与response,代理了这两个对象的方法与属性。其中使用了Tj写的node-delegates库,用于代理context.request与context.response上的方法与属性。

    /**
     * Response delegation
     */
    
    delegate(proto, 'response')
    .method('attachment')
    .method('redirect')
    .method('remove')
    .method('vary')
    .method('set')
    .method('append')
    .method('flushHeaders')
    .access('status')
    .access('message')
    .access('body')
    .access('length')
    .access('type')
    .access('lastModified')
    .access('etag')
    .getter('headerSent')
    .getter('writable')
    // ...
    

    context除了代理这两个模块外,还包含一个请求异常时的错误处理函数。在application.js的callback()众使用了这个函数。

    const onerror = err => ctx.onerror(err);
    const handleResponse = () => respond(ctx);
    onFinished(res, onerror);
    return fn(ctx).then(handleResponse).catch(onerror);
    

    Context@onerror

    context.onerror(err)首先对传入的err变量进行判断,当err为空时退出函数,或者当err不为空且不为Error类型时抛出异常。

    if (null == err) return;
    if (!(err instanceof Error))  err = new Error('non-error thrown: ${err}');
    

    接着触发app自身的error事件,将错误抛给app。
    在此之前,设置headerSent变量表示响应头是否发送,若响应头已发送,或者不可写(即无法在响应中添加错误信息等),则退出该函数。

    let headerSent = false;
    if (this.headerSent || !this.writable) {
      headerSent = err.headerSent = true;
    }
    
    //  delegate
    this.app.emit('error', err, this);
    
    // nothing we can do here other
    // than delegate to the app-level
    // handler and log.
    if (headerSent) {
      return;
    }
    

    因为发生了错误,所以必须将之前中间设置的响应头信息清空。
    这里使用了Node提供的http.ServerResponse类上的getHeaderNames()与removeHeader()方法。但getHeaderNames()这个函数在Node 7.7版本时加入的,所以当没有提供该方法时需要使用_header来清空响应头。详情可见:Node.js#10805。

    //  first unset all headers
    if (typeof res.getHeaderNames === 'function') {
      res.getHeaderNames().forEach(name => res.removHeader(name));
    } else {
      res._headers = {};  //  Node < 7.7
    }
    

    清空之前中间件设置的响应头之后,将响应头设置为err.headers,并设置Content-Type与状态码。
    当错误码为ENOENT时,意味着找不到该资源,将状态码设置为404;当没有状态码或状态码错误时默认设置为500。

    //  then set those specified
    this.set(err.headers);
    
    //  force text/plain
    this.type = 'text';
    
    //  ENOENT support
    if ('ENOENT' == err.code) err.status = 404;
    
    //  default to 500
    if('number' != typeof err.status || !statuses[err.status]) err.status = 500;
    

    Request

    Request模块封装了请求相关的属性及方法。通过application中的createContext()方法,代理对应的request对象:

    const request = context.request = Object.create(this.request);
    // ...
    context.req = request.req = response.req = req;
    // ...
    request.response = response;
    

    Response

    Response模块封装了响应相关的属性以及方法。与request相同,通过createContext()方法代理对应的response对象:

    const response = context.response = Object.create(this.response);
    // ...
    context.res = request.res = response.res = res;
    // ...
    response.request = request;
    

    相关文章

      网友评论

          本文标题:Koa2源码分析

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