Koa 入门 -->搭建项目 --> 源码解析 --> 手写源码(三)

108 阅读6分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第27天,点击查看活动详情

koa 源码解析

package.json 文件的 main 字段可以得出,入口文件为 lib/application.js

class Application extends Emitter {}

Application 类继承自 Emitter 类,class Emitter 是 node 内置模块。

class Application extends Emitter {
    
  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;
    // 保存通过app.use(middleware)注册的中间件
    this.middleware = [];
    // Object.create => 都是对象
    // Context对象里就封装了上下文的所有变量和对象。
    this.context = Object.create(context);
    this.request = Object.create(request);
    this.response = Object.create(response);
  }

  /**
   * 调用原生 http 模块开启服务并监听端口
   */

  listen(...args) {
    // this.callback()返回的就是回调函数,所以callback是一个高阶函数。
    const server = http.createServer(this.callback());
    return server.listen(...args);
  }


  /**
   * 挂载中间件
   * use方法很简单,接受一个函数作为参数,并加入middleware数组。由于koa最开始支持使用generator函数作为中间件使用,但将在3.x的版本中放弃这项支持,因此koa2中对于使用generator函数作为中间件的行为给与未来将被废弃的警告,但会将generator函数转化为async函数。返回this便于链式调用。
   * @param {Function} fn
   * @return {Application} self
   * @api public
   * 只是把传入的回调函数先存到middleware数组里。
   */

  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 || '-');
    // 用app.use的时候,实际上就是将函数push进middleware数组中,等待之后的调用。 
    this.middleware.push(fn);
    return this;
  }

  /**
   * 执行返回 http 请求的回调函数
   * 
   * 把handleRequest返回出去,供http.createServer回调,注意这里形成了一个闭包,能获得fn
   * @returns fn
   */
  callback() {
    // compose 函数将中间件数组转换成执行链函数 fn
    // 创建洋葱模型的入口函数
    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;
  }
}

koa-compose 源码(洋葱模型实现)

compose 函数将中间件数组转换成执行链函数 fn

(源码部分在 node_modules/koa-compose)

/**
 * 接收一个middleware数组为参数,返回一个函数
 */

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} 返回Promise
   */
  return function (context, next) {
    // last called middleware #
    let index = -1
    // 返回第一个 use 的中间件函数
    return dispatch(0)
      
    // 每个 dispatch 的返回值也是一个 Promise,因此我们的中间件中可以方便地使用async函数进行定义,内部使用await next()调用“下游”,然后控制流回“上游”,这是更准确也更友好的中间件模型
    function dispatch (i) {
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      index = i
      // 获取当前传入下标的中间件函数
      let fn = middleware[i]
      // 防止最后一个中间件执行next进行无限循坏
      if (i === middleware.length) fn = next
      if (!fn) return Promise.resolve()
      try {
        // 中间件的执行是异步的,并且中间件执行完毕后返回的是一个Promise
        // 洋葱模型的触发方式,先执行当前的中间,并将下一个中间作为第二个参数(next)传入当前中间件中
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

类似于这样的结构

const [fn1, fn2, fn3] = this.middleware;

// 先执行当前的中间,并将下一个中间作为第二个参数(next)传入当前中间件中
const fnMiddleware = function (context) {
  return Promise.resolve(
    fn1(context, function next() {
      return Promise.resolve(
        fn2(context, function next() {
          return Promise.resolve(
            fn3(context, function next() {
              return Promise.resolve();
            })
          )
        })
      )
    })
  );
};

fnMiddleware(ctx).then(handleResponse).catch(onerror);

koa-convert 源码(转换 generator 函数)

koa1中主要是generator函数。koa2中会自动转换generator函数。

app.use时有一层判断,是否是generator函数,如果是则用koa-convert暴露的方法convert来转换重新赋值,再存入middleware,后续再使用。

(源码部分在 node_modules/koa-convert)

function convert (mw) {
  if (typeof mw !== 'function') {
    throw new TypeError('middleware must be a function')
  }
  if (mw.constructor.name !== 'GeneratorFunction') {
    // assume it's Promise-based middleware
    return mw
  }
  const converted = function (ctx, next) {
    // 转换重新赋值,再存入middleware,后续再使用
    return co.call(ctx, mw.call(ctx, createGenerator(next)))
  }
  converted._name = mw._name || mw.name
  return converted
}

function * createGenerator (next) {
  return yield next()
}

co 源码

首先要明白 generator 函数是不会自动执行的,需要一步步调用 next()。 co 就是让 generator向 async、await 函数一样自动执行。

(源码部分在 node_modules/co)

function co(gen) {
  var ctx = this;
  var args = slice.call(arguments, 1)

  return new Promise(function(resolve, reject) {
    if (typeof gen === 'function') gen = gen.apply(ctx, args);
    if (!gen || typeof gen.next !== 'function') return resolve(gen);

    onFulfilled();


    function onFulfilled(res) {
      var ret;
      try {
        ret = gen.next(res);
      } catch (e) {
        return reject(e);
      }
      next(ret);
    }


    function onRejected(err) {
      var ret;
      try {
        ret = gen.throw(err);
      } catch (e) {
        return reject(e);
      }
      next(ret);
    }

    // 反复执行调用自己
    function next(ret) {

      // 检查当前是否为 Generator 函数的最后一步,如果是就返回
      if (ret.done) return resolve(ret.value);
      // 确保返回值是promise对象
      var value = toPromise.call(ctx, ret.value);
      // 使用 then 方法,为返回值加上回调函数,然后通过 onFulfilled 函数再次调用 next 函数
      if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
      // 在参数不符合要求的情况下(参数非 Thunk 函数和 Promise 对象),将 Promise 对象的状态改为 rejected,从而终止执行。
      return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
        + 'but the following object was passed: "' + String(ret.value) + '"'));
    }
  });
}

小结👉👉👉:

koa-compose是将app.use添加到middleware数组中的中间件(函数),通过使用Promise串联起来,next()返回的是一个promise

koa-convert 判断app.use传入的函数是否是generator函数,如果是则用koa-convert来转换,最终还是调用的co来转换。

co源码实现原理:其实就是通过不断的调用generator函数的next()函数,来达到自动执行generator函数的效果(类似async、await函数的自动自行)。


以上是整个 koa 洋葱模型的核心概念。

application.js 文件中还定义了一些用于请求和响应的函数

handleRequest --- 请求真正的回调函数

/**
  * 请求真正的回调函数
  * 核心在于执行 fnMiddleware,这个其实就是 const fn = compose(this.middleware)
  * compose会串联顺序执行中间件 next 之前的代码,完成后倒序执行各中间件next之后的代码
 */

handleRequest(ctx, fnMiddleware) {
  const res = ctx.res;
  // 一开始就将res的statusCode定义为404
  // 如果在我们没有设置body的情况下,默认就会返回404。
  res.statusCode = 404;
  const onerror = err => ctx.onerror(err);
  const handleResponse = () => respond(ctx);
  onFinished(res, onerror);
  return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}

respond --- 请求在经过所有中间件全部处理后的自动响应函数

function respond(ctx) {
  // 当 ctx 的 respond 为false 直接返回
  if (false === ctx.respond) return;

  // 当请求是scoket将根据socket的writable,否则都未true
  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();
  }

  // 请求是HEAD的一些处理
  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

  // 处理 Buffer 类型返回
  if (Buffer.isBuffer(body)) return res.end(body);
  // 处理字符串类型返回
  if ('string' === typeof body) return res.end(body);
  // 处理 Stream 类型返回
  if (body instanceof Stream) return body.pipe(res);

  // body: json 对象处理,转为JSON字符串返回
  body = JSON.stringify(body);
  if (!res.headersSent) {
    ctx.length = Buffer.byteLength(body);
  }
  res.end(body);
}

createContext --- 根据 req,res 创建上下文对象

/**
 * 根据 req,res 创建上下文对象
 * @api private
 * 每次回调过来都是创建新的context、request和response实例,这样本质上也对应了HTTP是无状态的。
 */

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;
  // 注意context、request、response都有成员变量双向绑定在一起,从一个对象上能获取另外两个对象,后面可以看到很多context上的方法都被代理到了request和response上。
  request.ctx = response.ctx = context;
  request.response = response;
  response.request = request;
  context.originalUrl = request.originalUrl = req.url;
  //公共的存储内容,后端模板也会把这里的属性作为视图的上下文用于渲染
  context.state = {};
  return context;
}

context.js

// COOKIES是一个Symbol类型的私有变量
const COOKIES = Symbol('context#cookies');

const proto = module.exports = {

  /**
   * util.inspect() implementation, which
   * just returns the JSON output.
   *
   * @return {Object}
   * @api public
   */

  inspect() {
    if (this === proto) return this;
    return this.toJSON();
  },

  /**
   * 获取当前ctx的内容
   */

  toJSON() {
    return {
      request: this.request.toJSON(),
      response: this.response.toJSON(),
      app: this.app.toJSON(),
      originalUrl: this.originalUrl,
      req: '<original node req>',
      res: '<original node res>',
      socket: '<original node socket>'
    };
  },

  /**
   * http-assert,对http-errors的封装,一些基本的断言并设置http返回体
   */

  assert: httpAssert,

  throw(...args) {
    throw createError(...args);
  },

  /**
   *
   * @param {Error} err
   * @api private
   */

  onerror(err) {
    // 没有错误时什么也不做
    if (null == err) return;

    // err不是Error实例时,使用err创建一个Error实例
    const isNativeError =
      Object.prototype.toString.call(err) === '[object Error]' ||
      err instanceof Error;
    if (!isNativeError) err = new Error(util.format('non-error thrown: %j', err));

    // headerSent变量表示响应头是否发送,如果响应头已经发送,或者响应处于不可写状态,将无法在响应中添加错误信息,直接退出该函数,否则需要将之前写入的响应头部信息清空。
    let headerSent = false;
    if (this.headerSent || !this.writable) {
      headerSent = err.headerSent = true;
    }

    // 触发koa实例app的error事件
    this.app.emit('error', err, this);

    // nothing we can do here other
    // than delegate to the app-level
    // handler and log.
    if (headerSent) {
      return;
    }

    const { res } = this;

    // 移除所有设置过的响应头
    if (typeof res.getHeaderNames === 'function') {
      res.getHeaderNames().forEach(name => res.removeHeader(name));
    } else {
      res._headers = {}; // Node < 7.7
    }

    // 设置错误头部
    this.set(err.headers);

    // 设置错误时的Content-Type
    this.type = 'text';

    let statusCode = err.status || err.statusCode;

    // 找不到文件错误码设为404
    if ('ENOENT' === err.code) statusCode = 404;

    // 不能被识别的错误将错误码设为500
    if ('number' !== typeof statusCode || !statuses[statusCode]) statusCode = 500;

    // respond
    const code = statuses[statusCode];
    const msg = err.expose ? err.message : code;
    this.status = err.status = statusCode;
    this.length = Buffer.byteLength(msg);
    res.end(msg);
  },

  // 处理Cookie
  get cookies() {
    if (!this[COOKIES]) {
      this[COOKIES] = new Cookies(this.req, this.res, {
        keys: this.app.keys,
        secure: this.request.secure
      });
    }
    return this[COOKIES];
  },

  set cookies(_cookies) {
    this[COOKIES] = _cookies;
  }
};

/**
 * 本质上是一个构造函数,接受两个参数,第一个参数是代理对象,第二个参数是被代理的对象
 *
 * context使用了delegates这个库。主要是将context中的一些值和函数代理到request和response中,这样实际上我们调用ctx.hostname获取值的时候,实际上是调用了req.hostname。从而方便调用。
 */

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');

request.js 和 response.js 是对原生 req 和 res 的封装,同时也提供一些额外的值和函数。

整体流程🔎🔎:

初始化阶段: new初始化一个实例,use搜集中间件到middleware数组,listen 合成中间件fnMiddleware,返回一个callback函数给http.createServer,开启服务器,等待http请求。

请求阶段: 每次请求,createContext生成一个新的ctx,传给fnMiddleware,触发中间件的整个流程。

响应阶段: 整个中间件完成后,调用respond方法,对请求做最后的处理,返回响应给客户端。