koa源码分析

本文深入剖析了Koa框架的源码,包括核心的application.js、context.js、request.js和response.js。Koa通过async函数改进错误处理,并提供了一个中间件系统。文章详细介绍了中间件的工作原理,如洋葱圈模型,以及compose和delegate这两个关键工具的作用。同时,讨论了错误处理机制,包括在application.js和context.js中的onerror方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

koa源码分析

一、Koa介绍

Koa 是一个新的 web 框架,由 Express 幕后的原班人马打造, 致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石。 通过利用 async 函数丢弃回调函数,并有力地增强错误处理Koa 并没有捆绑任何中间件, 而是提供了一套优雅的方法,帮助开发者快速而愉快地编写服务端应用程序。

相对使用koa本身,koa更重要的是成为了一个基石。由于koa对开发者的束缚更小,让开发者可以基于koa去完成功能更丰富的框架,比如gulu和egg就是基于koa的二次封装。

二、Koa源码分析

Koa主要包含4个js文件,包括application.js,context.js,request.js, response.js。

  • application:koa应用实例。
  • context:一次请求的上下文环境。
  • request:基于node原生req对象封装后的koa的request对象。
  • response:基于node原生res对象封装后的koa的response对象。

流程:
在这里插入图片描述

1. application.js

  • 构造函数,暴露了Application类,继承了Emitter模块。构造函数做了以下几件事。
    • 默认不设置代理
    • 初始化中间件数组middleware
    • 子域名偏移量默认为2,也就是默认忽略数量为2
      • 假设域名是"tobi.ferrets.example.com"。如果app.subdomainOffset没有设置,也就是说默认要忽略的偏移量是2,那么ctx.subdomains是[“ferrets”, “tobi”]。
    • 环境变量的处理
    • 挂载context、request、response
    • 重写util.inpsect方法
    • util.inspect是一个将任意对象转换为字符串的方法,通常用于调试和错误输出。在node 6+版本中,util.inspect.custom返回一个Symbol类型。对象用util.inspect.custom作为key,值为函数的话,在对对象使用util.inspect()时候,util.inpsect会被该函数覆盖。
module.exports = class Application extends Emitter {
 /**
  * Initialize a new `Application`.
  *
  * @api public
  */
 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;
  }
 }
  • only方法返回对象白名单属性,即返回只想对外暴露的属性。此处表明只对外暴露‘subdomaimOffset’、‘proxy’以及‘env’三个属性。
 /**
  * Return JSON representation.
  * We only bother showing settings.
  *
  * @return {Object}
  * @api public
  */
 toJSON() {
  return only(this, [
   'subdomainOffset',
   'proxy',
   'env'
  ]);
 }
  • 这时候再利用util.inspect将app实例转换为字符串的话,只能看到经过only处理后暴露的属性。
 /**
  * Inspect implementation.
  *
  * @return {Object}
  * @api public
  */
 inspect() {
  return this.toJSON();
 }
  • 服务器的启动,调用Node.js的http模块来创建一个服务器,具体的handler是使用callback方法的返回值。通过(…args)将listen的参数转发给创建好的server。
  /**
  * 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);
 }
}
  • this.callback()返回一个handleRequest函数处理请求
    • callback函数首先利用compose组合已注册的中间件,形成函数fn
    • 利用了闭包的性质,将fn持久化,不用每次接收到请求后都重新对中间件进行组合
    • 此时每次到达一个新的请求,都会执行一次handleRequest
      • 该函数首先会结合node的生的req和res对象创建一个请求上下文ctx
      • 然后将ctx和fn一起传入this.handleRequest函数进行处理,返回结果
 /**
  * Return a request handler callback
  * for node's native http server.
  *
  * @return {Function}
  * @api public
  */
 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;
 }
  • 该函数进行了以下操作,这些操作的目的是为了让开发者更加自由的去访问某一个对象
    • 首先创建了一个context对象
    • 然后基于node原生的req和res创建koa实例的request和response对象,并挂载到context上
    • 将koa实例分别挂载到context、request、response上
    • 将node原生req分别挂载到context、request、response上
    • 将node原生res分别挂载到context、request、response上
    • 将context挂载到request、response上
    • 将resquest和response分别互相挂载
    • 将req.url挂载到context和request上
 /**
  * Initialize a new context.
  *
  * @api private
  */
 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;
 }
  • handleMiddleware,处理请求,并执行compose后的中间件
    • 首先获取res对象,将其statusCode设置为默认值404
    • 然后利用onFinished函数侦听res,如果响应过程出错,回调用ctx.onerror on-finished
      • onFinished(res, listener)添加一个监听器去监听响应的完成情况
      • 当响应顺利完成时,监听器会在响应完成后被调用一次
      • 如果响应已完成,监听器将会被执行
      • 如果响应完成但是过程中出错了,监听器的第一个参数会是对应的error
      • 用来处理响应结束后的相关资源释放工作&&异常处理
    • 最后利用fnMiddleware处理ctx,处理完成后,将处理后的ctx传递给handleResponse,也就是respond函数,完成对客户端的响应
    • 如果过程中发生错误,则调用ctx.onerror进行错误处理
 /**
  * Handle request in callback.
  *
  * @api private
  */
 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);
 }
  • 通过use方法,将中间件注册到app实例上,主要是加入到middleware数组里。
    • 首先进行了类型判断,如果参数不是function则抛出错误。
    • 然后判断注册的中间件函数是不是generator函数,如果是的话,将其转化为async/await形式。同时提示开发者,generator函数在v3版本不再支持。这和koa v1有关,因为koa v1的中间件函数默认使用的就是generator函数,而koa v2引入了async/await。
    • 将该函数加入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;
 }
  • 用于处理响应。
  • 为了绕过 Koa 的内置 response 处理,你可以显式设置 ctx.respond = false;。 如果您想要写入原始的 res 对象而不是让 Koa 处理你的 response,请使用此参数。但是需要注意,Koa 支持使用此功能。这可能会破坏 Koa 中间件和 Koa 本身的预期功能。使用这个属性被认为是一个 hack,只是便于那些希望在 Koa 中使用传统的 fn(req, res) 功能和中间件的人。
function respond(ctx) {
 // allow bypassing koa
 if (false === ctx.respond) return;
 if (!ctx.writable) return;
 //code here
}
  • onerror见错误处理

2. context.js

  • context中有两部分,一部分是自身的属性,用作框架内使用。(onerror见错误处理)
    • toJSON、inspect同上
    • cookies的getter和setter
 /**
  * util.inspect() implementation, which
  * just returns the JSON output.
  *
  * @return {Object}
  * @api public
  */
 inspect() {
  if (this === proto) return this;
  return this.toJSON();
 },
 /**
  * Return JSON representation.
  *
  * Here we explicitly invoke .toJSON() on each
  * object, as iteration will otherwise fail due
  * to the getters and cause utilities such as
  * clone() to fail.
  *
  * @return {Object}
  * @api public
  */
 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>'
  };
 },
 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;
 }
  • 另一部分是Request和Response委托的操作方法,主要为提供给我们更方便从Request获取想要的参数和设置Response内容,它用到的是delegates三方库,把request,esponse 对象上的属性和方法代理到context 对象上。
  • 比如经常使用的ctx.body便是将response.body的body属性委托到了ctx上,这样可以让代码更加简洁。此时 ctx.body === response.body
  • ctx.body = {}
/**
 * Response delegation.
 */
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');
/**
 * Request delegation.
 */
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');

3. request.js && response.js

  • 对原生的 http 模块的 request对象进行封装,提供请求相关的数据与操作,使用es6的get和set方法,重新定义并暴露api。
  • request:包含了一些操作Node原生请求对象的方法,如获取query数据,获取请求url等。
    • 源码连接:https://github.com/koajs/koa/blob/master/lib/request.js
    • 各属性与方法作用,参考https://koa.bootcss.com/#request
    • setter:
      • this.req.url,header, headers, url, method, path, query, querystring, search,
    • getter:
      • header, headers, url, origin, href, method, path, query, querystring, search, host, hostname, URL, fresh, stale, idempotent, socket, charset, length, protocol, secure, ips, ip, subdomains, accept, type
    • method:
      • accepts、acceptsEncodings、acceptsLanguages、get、is、inspect、toJSON
  • response:包含了一些用于设置状态码,主体数据,header等一些用于操作响应请求的方法。
    • 源码链接:https://github.com/koajs/koa/blob/master/lib/response.js
    • 各属性与方法作用,参考https://koa.bootcss.com/#response
    • setter:
      • status, message, body, length, type, lastModified, etag
    • getter:
      • socket, header, headers, status, message, body, length, headerSent, lastModified, etag, type
    • method:
      • attachment、redirect、remove、vary、has、set、append、flushHeaders、inspect、toJSON

4. 中间件

  • 简介
    • Koa最大的特色和最优的设计就是中间件,就是在匹配路由之前和匹配路由之后执行函数。
    • 使用app.use()加载中间件。每个中间件接收两个参数,ctx对象和next函数,通过调用next将执行权交给下一个中间件。
  • 分类
    • 应用级中间件(app.use)
      • 任何路由都会先经过应用级中间件,当执行完成next后再去匹配相应的路由
    • 路由级中间件(router.use)
      • 路由匹配过程中,对于相同路由会从上往下依次执行中间件,直到最后一个没有next参数的中间件为止
    • 错误处理中间件
      • 路由在匹配成功并执行完相应的操作后还会再次进入应用级中间件执行 next 之后的逻辑。所以对于404、500等错误可以在最外层的(第一个)应用级中间件的next之后做相应的处理。
      • 如果只有一个应用级中间件的话,顺序就无所谓所有路由中间件之前和之后了。
    • 第三方中间件
      • 类似于koa-router、koa-bodyparser等就是第三方中间件。
    • 内置中间件(比如express的内置router)
  • 洋葱圈模型
    • 洋葱模型是中间件的一种串行机制,并且是支持异步,第一个中间件函数中如果执行了next(),则下一个中间件会被执行,运行原理是基于以上提到的compose,下方有详细说明。
    • 基于洋葱圈模型,每个中间件都会执行两次。
    • ctx.throw,抛出错误,停止洋葱圈。

|[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-igz8wObb-1599143236417)(https://bytedance.feishu.cn/space/api/box/stream/download/asynccode/?code=19a83fab5c21d35079f046c3208d76a1_8f118824ce50c961_boxcnAUrlGA4PzPUdTLDYp3MVHg_YrhhpxL7bGrRjjGSLNz4HZPoseGVbzSN)]

5. 错误处理

  • application.js中的onerror
    • 绑定在 koa 实例对象上的,它监听的是整个对象的 error 事件,用来处理出错函数的堆栈打印, 方便我们进行问题定位。
 /**
  * Default error handler.
  *
  * @param {Error} err
  * @api private
  */
 onerror(err) {
  // When dealing with cross-globals a normal `instanceof` check doesn't work properly.
  // See https://github.com/koajs/koa/issues/1466
  // We can probably remove it once jest fixes https://github.com/facebook/jest/issues/2549.
  const isNativeError =
   Object.prototype.toString.call(err) === '[object Error]' ||
   err instanceof Error;
  if (!isNativeError) throw new TypeError(util.format('non-error thrown: %j', err));
  if (404 === err.status || err.expose) return;
  if (this.silent) return;
  const msg = err.stack || err.toString();
  console.error(`\n${msg.replace(/^/gm, '  ')}\n`);
 }
};
  • context.js中的onerror
    • 在中间函数数组生成的 Promise 的 catch 中与 res 对象的 onFinished 函数的回调应用到,为了处理请求或响应中出现的 error 事件
      • 在该函数中,首先判断错误的类型,以格式化对应的错误提示信息。
      • 之后通过headerSent头检测是否已经发送过一个响应头,如果已经发送过响应,则跳过后续部分。
      • 再之后通过this.app.emit(‘error’, err, this);触发了application的onerror函数,将出错函数的堆栈打印出来。
      • 如果之前没有发送过响应头,即headerSent为false并且响应是可写的。就进行响应数据的准备并返回给客户端,包括
        • 设置响应头为err.headers
        • 设置相应的Content-Type为text/plain
        • 设置响应状态码与响应数据
 /**
  * Default error handling.
  *
  * @param {Error} err
  * @api private
  */
 onerror(err) {
  // don't do anything if there is no error. 
  // this allows you to pass this.onerror
  // to node-style callbacks.
  if (null == err) return;
  // When dealing with cross-globals a normal `instanceof` check doesn't work properly.
  // See https://github.com/koajs/koa/issues/1466
  // We can probably remove it once jest fixes https://github.com/facebook/jest/issues/2549.
  const isNativeError =
   Object.prototype.toString.call(err) === '[object Error]' ||
   err instanceof Error;
  if (!isNativeError) err = new Error(util.format('non-error thrown: %j', err));
  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;
  }
  const { res } = this;
  // first unset all headers
  /* istanbul ignore else */
  if (typeof res.getHeaderNames === 'function') {
   res.getHeaderNames().forEach(name => res.removeHeader(name));
  } else {
   res._headers = {}; // Node < 7.7
  }
  // then set those specified
  this.set(err.headers);
  // force text/plain
  this.type = 'text';
  let statusCode = err.status || err.statusCode;
  // ENOENT support
  if ('ENOENT' === err.code) statusCode = 404;
  // default to 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);
 },
  • **异步异常捕获。基于洋葱圈中间件模型,**可以把 try { next() } 当成第一个中间件,如下,这样就可以集中处理各个中间件抛出的所有异步异常。
app.use(async function(ctx, next) {
 try {
  next();
 } catch (err) {
  this.status = err.status || 500;
  this.body = err.message;
  this.app.emit('error', err, this);
 }
});
app.use(async function(ctx, next) {
 throw new Error('some error');
})

三、 几个需要注意的点

1. compose——来自koa-compose包

  • 首先进行了参数类型判断
    • 要求参数middleware为数组,且middleware中所有元素类型都要是function
  • 然后以闭包的形式返回处理函数,延迟执行时机,以便于多次利用,不用每一次都重新组合中间件
  • 作为返回结果的函数中,中间件运行机制如下
    • dispatch(i)代表调用中间件数组middleware中第i个中间件函数
    • return dispatch(0)表示从第一个中间件开始调用
    • 利用index来避免一个中间件中出现两次next()调用,每一次调用都会更新index的值来配合这个检测
    • 每个中间件都在Promise.resolve()中执行,执行结果被resolve到上一层
    • 但由于context是引用类型,所有对context的修改都是同步更新的,这里的resolve只是配合await 的等待
      • async function(context, next){await next()}
      • 每次调用next方法,都会运行dispatch(i + 1)递归调用下一个中间,待执行结束,再执行本中间件的后续部分
      • 如果运行到最后一个中间件,则通过Promise.resolve()去resolve一个空值进行返回
'use strict'
/**
 * Expose compositor.
 */
module.exports = compose
/**
 * Compose `middleware` returning
 * a fully valid middleware comprised
 * of all those which are passed.
 *
 * @param {Array} middleware
 * @return {Function}
 * @api public
 */
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) { // 接受koa的ctx和next作为参数,处理中间件
  // last called middleware #
  let index = -1 // 防止next多次调用
  return dispatch(0)
  function dispatch (i) {
   if (i <= index) return Promise.reject(new Error('next() called multiple times'))
   // 每个中间件都有属于自己的一个闭包作用域,同一个中间件的 i 是不变的,而 index 是在闭包作用域外面的, 当同一个中间件调用第二个next时,
   // 此时index = 2, index > 1, 如果不加这种验证,则会执行dispatch(2)等,执行不到下一个中间件
   index = i
   let fn = middleware[i]
   if (i === middleware.length) fn = next // 最后一个中间件执行完后,自动调取next返回一个没有任何操作的resolve,结束流程
   if (!fn) return Promise.resolve()
   try {
    return Promise.resolve(fn(context, function next () { // 用Promise包裹中间件,方便await调用
     return dispatch(i + 1) // 通过递归的方式不断的运行中间件(跳到下一个中间件进行do something),从而形成洋葱中间件模式
    }))
   } catch (err) {
    return Promise.reject(err)
   }
  }
 }
}

2. delegate——来自delegates包

  • method,将目标对象的对应方法委托给指定对象
  • getter,将目标对象的对应属性的getter委托给指定对象
  • setter,将目标对象的对应属性的setter委托给指定对象
  • access,将目标对象的对应属性的getter和setter委托给指定对象
  • fluent,将目标对象的对应属性委托给指定对象,并提供了另外一种使用方法,可以通过「name()」获取该属性值,通过「name(val)」为该属性赋值。
/**
 * Delegate method `name`.
 *
 * @param {String} name
 * @return {Delegator} self
 * @api public
 */
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 accessor `name`.
 *
 * @param {String} name
 * @return {Delegator} self
 * @api public
 */
Delegator.prototype.access = function(name){
 return this.getter(name).setter(name);
};
/**
 * Delegator getter `name`.
 *
 * @param {String} name
 * @return {Delegator} self
 * @api public
 */
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;
};
/**
 * Delegator fluent accessor
 *
 * @param {String} name
 * @return {Delegator} self
 * @api public
 */
Delegator.prototype.fluent = function (name) {
 var proto = this.proto;
 var target = this.target;
 this.fluents.push(name);
 proto[name] = function(val){
  if ('undefined' != typeof val) {
   this[target][name] = val;
   return this;
  } else {
   return this[target][name];
  }
 };
 return this;
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值