美文网首页程序员
node读源码系列---koa2源码分析

node读源码系列---koa2源码分析

作者: potenstop | 来源:发表于2020-11-02 18:33 被阅读0次

    koa介绍

    koa有1.x和2.x两个版本,koa1.x基于generator,依赖co的包装。koa2.x基于promise,在node v7.6+可以直接运行,低版本可以babel。下面是基于koa2.13源码的介绍。

    简单使用

    // 1 导包
    const Koa = require('koa');
    // 2 new一个koa的实例 说明koa是一个类
    const app=new Koa();
    // 3 挂载中间件
    app.use(async function (ctx, next) {
        const startTime = new Date()
        console.log(`${startTime} ${ctx.method} ${ctx.url}`);
        await next();
        const endTime = new Date()
        console.log(`${endTime} ${endTime.getTime() - startTime.getTime()}ms`);
    });
    app.use(ctx => {
        ctx.body='hello world';
    });
    // 4 指定监听端口
    app.listen('3000', () => {
        console.log('start suc!!');
        console.log('http://127.0.0.1:3000');
    });
    

    目录结构和依赖

    lib目录下有四个文件:

    • application.js: 应用程序。
    • context.js:上下文。
    • request.js: 请求类。
    • response.js: 响应类。
    {
      "dependencies": {
        "accepts": "^1.3.5",
        "cache-content-type": "^1.0.0",
        "content-disposition": "~0.5.2",
        "content-type": "^1.0.4",
        "cookies": "~0.8.0",
        "debug": "~3.1.0",
        "delegates": "^1.0.0",
        "depd": "^1.1.2",
        "destroy": "^1.0.4",
        "encodeurl": "^1.0.2",
        "escape-html": "^1.0.3",
        "fresh": "~0.5.2",
        "http-assert": "^1.3.0",
        "http-errors": "^1.6.3",
        "is-generator-function": "^1.0.7",
        "koa-compose": "^4.1.0",
        "koa-convert": "^1.2.0",
        "on-finished": "^2.3.0",
        "only": "~0.0.2",
        "parseurl": "^1.3.2",
        "statuses": "^1.5.0",
        "type-is": "^1.6.16",
        "vary": "^1.1.2"
      },
      "deprecated": false,
      "description": "Koa web app framework",
      "devDependencies": {
        "egg-bin": "^4.13.0",
        "eslint": "^6.5.1",
        "eslint-config-koa": "^2.0.0",
        "eslint-config-standard": "^14.1.0",
        "eslint-plugin-import": "^2.18.2",
        "eslint-plugin-node": "^10.0.0",
        "eslint-plugin-promise": "^4.2.1",
        "eslint-plugin-standard": "^4.0.1",
        "gen-esm-wrapper": "^1.0.6",
        "mm": "^2.5.0",
        "supertest": "^3.1.0"
      },
      "engines": {
        "node": "^4.8.4 || ^6.10.1 || ^7.10.1 || >= 8.1.4"
      },
      "exports": {
        ".": {
          "require": "./lib/application.js",
          "import": "./dist/koa.mjs"
        },
        "./": "./"
      },
      "files": [
        "dist",
        "lib"
      ],
      "main": "lib/application.js",
      "name": "koa",
      "version": "2.13.0"
    }
    

    入口文件: lib/application.js

    一、 application.js

    module.exports = class Application extends Emitter
    ...
    module.exports.HttpError = HttpError;
    

    Application 是继承Emitter的类,并且同时导出了HttpErrorr。为什么会继承Emitter?往下看。

    1 constructor

    constructor(options) {
        super();
        options = options || {};
        this.proxy = options.proxy || false;
        this.subdomainOffset = options.subdomainOffset || 2;
        this.proxyIpHeader = options.proxyIpHeader || 'X-Forwarded-For';
        this.maxIpsCount = options.maxIpsCount || 0;
        this.env = options.env || process.env.NODE_ENV || 'development';
        if (options.keys) this.keys = options.keys;
        this.middleware = [];
        this.context = Object.create(context);
        this.request = Object.create(request);
        this.response = Object.create(response);
        // util.inspect.custom support for node 6+
        /* istanbul ignore else */
        if (util.inspect.custom) {
          this[util.inspect.custom] = this.inspect;
        }
      }
    

    option配置分以下几种
    1 主要配置反向代理,客户端ip相关的配置(proxy subdomainOffset proxyIpHeader maxIpsCount )
    2 middleware是中间件数组。
    3 响应头、请求头、上下文(request response Context) 通过Object.create继承原有对象的属性并且创建新的对象。
    4 util.inspect.custom是一个Symbol类型的值,通过定义对象的[util.inspect.custom]属性为一个函数,简单来说就是自定义对象的查看函数。

    2 use

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

    由于koa最开始支持使用generator函数作为中间件使用,如果是generator函数这里使用convert进行了一次转换,并且push到了middleware数组中。

    3 listen

     listen(...args) {
        debug('listen');
        const server = http.createServer(this.callback());
        return server.listen(...args);
      }
    
    callback() {
        const fn = compose(this.middleware);
    
        if (!this.listenerCount('error')) this.on('error', this.onerror);
    
        const handleRequest = (req, res) => {
          const ctx = this.createContext(req, res);
          return this.handleRequest(ctx, fn);
        };
    
        return handleRequest;
      }
    

    listen方法先调用http模块的createServer, 传入回调函数callback,listen方法的入参合http的listen的入参是一致的。创建http服务成功之后会调用callback函数。先调用compose返回fn。之后就是handleRequest生成ctx,并按中间件加载的顺序执行逻辑了。到这里就可以回到继承Emitter,因为这里用到事件触发器(on和emit)完成事件监听和发送。

    4 createContext(req, res)

    createContext(req, res) {
        const context = Object.create(this.context);
        const request = context.request = Object.create(this.request);
        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.ctx = response.ctx = context;
        request.response = response;
        response.request = request;
        context.originalUrl = request.originalUrl = req.url;
        context.state = {};
        return context;
      }
    

    createContext主要做的就是构造一个ctx对象。

    5 handleRequest

    handleRequest(ctx, fnMiddleware) {
        const res = ctx.res;
        res.statusCode = 404;
        const onerror = err => ctx.onerror(err);
        const handleResponse = () => respond(ctx);
        onFinished(res, onerror);
        return fnMiddleware(ctx).then(handleResponse).catch(onerror);
      }
    

    fnMiddleware就是koa-compose函数的返回值

    6 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!')
      }
    
      /**
       * @param {Object} context
       * @return {Promise}
       * @api public
       */
    
      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, dispatch.bind(null, i + 1)));
          } catch (err) {
            return Promise.reject(err)
          }
        }
      }
    }
    

    按app.use的顺序执行,形成了洋葱模型。compose的返回值是一个匿名函数,有context和next两个函数。首次执行时候context是由fnMiddleware函数传入的,next为undefined。fn为执行当前次的中间件函数,next其实就是为dispatch函数。整体上使用递归+闭包的方式进行了实现。

    7 respond(ctx)

      // allow bypassing koa
      if (false === ctx.respond) return;
    
      if (!ctx.writable) return;
    
      const res = ctx.res;
      let body = ctx.body;
      const code = ctx.status;
    
      // ignore body
      if (statuses.empty[code]) {
        // strip headers
        ctx.body = null;
        return res.end();
      }
    
      if ('HEAD' === ctx.method) {
        if (!res.headersSent && !ctx.response.has('Content-Length')) {
          const { length } = ctx.response;
          if (Number.isInteger(length)) ctx.length = length;
        }
        return res.end();
      }
    
      // status body
      if (null == body) {
        if (ctx.response._explicitNullBody) {
          ctx.response.remove('Content-Type');
          ctx.response.remove('Transfer-Encoding');
          return res.end();
        }
        if (ctx.req.httpVersionMajor >= 2) {
          body = String(code);
        } else {
          body = ctx.message || String(code);
        }
        if (!res.headersSent) {
          ctx.type = 'text';
          ctx.length = Buffer.byteLength(body);
        }
        return res.end(body);
      }
    
      // responses
      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);
    }
    

    当所有的中间件都成功执行了,就会进到respond函数里,这里会判断ctx.body是不是有值的,如果没有值则返回404页面。

    二、request.js

    整体的属性就是对http.req中比较重要的字段重新定义了出来,比如length字段,就是res.header.Content-Length字段。

    三、response.js

    挑出几个比较常用的。

    1 set body

    set body(val) {
        const original = this._body;
        this._body = val;
    
        // no content
        if (null == val) {
          if (!statuses.empty[this.status]) this.status = 204;
          if (val === null) this._explicitNullBody = true;
          this.remove('Content-Type');
          this.remove('Content-Length');
          this.remove('Transfer-Encoding');
          return;
        }
    
        // set the status
        if (!this._explicitStatus) this.status = 200;
    
        // set the content-type only if not yet set
        const setType = !this.has('Content-Type');
    
        // string
        if ('string' === typeof val) {
          if (setType) this.type = /^\s*</.test(val) ? 'html' : 'text';
          this.length = Buffer.byteLength(val);
          return;
        }
    
        // buffer
        if (Buffer.isBuffer(val)) {
          if (setType) this.type = 'bin';
          this.length = val.length;
          return;
        }
    
        // stream
        if (val instanceof Stream) {
          onFinish(this.res, destroy.bind(null, val));
          if (original != val) {
            val.once('error', err => this.ctx.onerror(err));
            // overwriting
            if (null != original) this.remove('Content-Length');
          }
    
          if (setType) this.type = 'bin';
          return;
        }
    
        // json
        this.remove('Content-Length');
        this.type = 'json';
      }
    

    当ctx.body = ''时候调用。有下面几种场景。
    1 ctx.body = null 或者undefined时候,相当于状态码是204
    2 ctx.body非null 则 状态码为200
    3 ctx.body为字符串类型,则Type为html或text
    4 ctx.body为buffer类型,则Type为bin
    5 ctx.body为Stream类型,则Type为bin
    5 ctx.body其他,则Type为json

    2 status

    set status(code) {
        if (this.headerSent) return;
    
        assert(Number.isInteger(code), 'status code must be a number');
        assert(code >= 100 && code <= 999, `invalid status code: ${code}`);
        this._explicitStatus = true;
        this.res.statusCode = code;
        if (this.req.httpVersionMajor < 2) this.res.statusMessage = statuses[code];
        if (this.body && statuses.empty[code]) this.body = null;
      }
    

    四、context.js

    重点看下别名怎么实现的。如ctx.body是ctx.response.body的别名

    delegate(proto, 'response')
      .method('attachment')
      .method('redirect')
      .method('remove')
      .method('vary')
      .method('has')
      .method('set')
      .method('append')
      .method('flushHeaders')
      .access('status')
      .access('message')
      .access('body')
      .access('length')
      .access('type')
      .access('lastModified')
      .access('etag')
      .getter('headerSent')
      .getter('writable');
    
    delegate(proto, 'request')
      .method('acceptsLanguages')
      .method('acceptsEncodings')
      .method('acceptsCharsets')
      .method('accepts')
      .method('get')
      .method('is')
      .access('querystring')
      .access('idempotent')
      .access('socket')
      .access('search')
      .access('method')
      .access('query')
      .access('path')
      .access('url')
      .access('accept')
      .getter('origin')
      .getter('href')
      .getter('subdomains')
      .getter('protocol')
      .getter('host')
      .getter('hostname')
      .getter('URL')
      .getter('header')
      .getter('headers')
      .getter('secure')
      .getter('stale')
      .getter('fresh')
      .getter('ips')
      .getter('ip');
    

    可以看出来是通过delegate,深入到delegate内部看下

    Delegator.prototype.method = function(name){
      var proto = this.proto;
      var target = this.target;
      this.methods.push(name);
    
      proto[name] = function(){
        return this[target][name].apply(this[target], arguments);
      };
    
      return this;
    };
    Delegator.prototype.getter = function(name){
      var proto = this.proto;
      var target = this.target;
      this.getters.push(name);
    
      proto.__defineGetter__(name, function(){
        return this[target][name];
      });
    
      return this;
    };
    
    /**
     * Delegator setter `name`.
     *
     * @param {String} name
     * @return {Delegator} self
     * @api public
     */
    
    Delegator.prototype.setter = function(name){
      var proto = this.proto;
      var target = this.target;
      this.setters.push(name);
    
      proto.__defineSetter__(name, function(val){
        return this[target][name] = val;
      });
    
      return this;
    };
    

    get、set其实就是通过defineGetterdefineSetter进行实现的。

    五、总结

    koa中间件执行流程图

    image.png

    相关文章

      网友评论

        本文标题:node读源码系列---koa2源码分析

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