百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

源码解析「Koa」(源码分析软件)

ccwgpt 2024-09-21 13:36 34 浏览 0 评论

基于 Node.js 平台的下一代 web 开发框架

koa 的源码位于 lib 目录,结构非常简单和清晰,只有四个文件:

application.js
context.js
request.js
response.js

Application类

Application 类定义继承自 Emitter.prototype ,这样在实例化Koa后,可以很方便的在实例对象中调用 Emitter.prototype 的原型方法。

/**
 * Expose `Application` class.
 * Inherits from `Emitter.prototype`.
 */
module.exports = 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;
    this.middleware = [];
    this.context = Object.create(context);
    this.request = Object.create(request);
    this.response = Object.create(response);
    if (util.inspect.custom) {
      this[util.inspect.custom] = this.inspect;
    }
  }
}

上面的构造函数中,定义了Application实例的11个属性:

属性含义proxy表示是否开启代理,默认为false。如果开启代理,对于获取request请求中的host,protocol,ip分别优先从Header字段中的 X-Forwarded-Host , X-Forwarded-Proto , X-Forwarded-For 获取。subdomainOffset子域名的偏移量,默认值为2,这个参数决定了request.subdomains的返回结果。proxyIpHeader代理的 ip 头字段,默认值为 X-Forwarded-For 。maxIpsCount最大的ips数,默认值为0,如果设置为大于零的值,ips获取的值将会返回截取后面指定数的元素。envkoa的运行环境, 默认是development。keys设置签名cookie密钥,在进行cookie签名时,只有设置 signed 为 true 的时候,才会使用密钥进行加密。middleware存放中间件的数组。context中间件第一个实参ctx的原型,定义在 context.j s中。requestctx.request的原型,定义在 request.js 中。responsectx.response的原型,定义在 response.js 中。[util.inspect.custom]util.inspect 这个方法用于将对象转换为字符串, 在node v6.6.0及以上版本中 util.inspect.custom 是一个Symbol类型的值,通过定义对象的[util.inspect.custom]属性为一个函数,可以覆盖 util.inspect 的默认行为。

use(fn)

use(fn) 接受一个函数作为参数,并加入到 middleware 数组。由于koa最开始支持使用generator函数作为中间件使用,但将在3.x的版本中放弃这项支持,因此koa2中对于使用 generator 函数作为中间件的行为给予未来将被废弃的警告,但会将 generator 函数转化为 async 函数。返回 this 便于链式调用。

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

listen(… args )

可以看到内部是通过原生的 http 模块创建服务器并监听的,请求的回调函数是 callback 函数的返回值。

/**
   * 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);
  }

callback()

compose将中间件数组转换成执行链函数fn, compose的实现是重点,下文会分析。koa继承自Emitter,因此可以通过listenerCount属性判断监听了多少个error事件, 如果外部没有进行监听,框架将自动监听一个error事件。callback函数返回一个handleRequest函数,因此真正的请求处理回调函数是handleRequest。在handleRequest函数内部,通过createContext创建了上下文ctx对象,并交给koa实例的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;
  }

handleRequest(ctx, fnMiddleware)

该方法最终将中间件执行链的结果传递给respond函数,经过respond函数的处理,最终将数据渲染到浏览器端。

/**
   * 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);
  }

respond(ctx)

respond是koa服务响应的最终处理函数,它主要功能是判断ctx.body的类型,完成最后的响应。另外,如果在koa中需要自行处理响应,可以设置ctx.respond = false,这样内置的respond就会被忽略。

/**
 * Response helper.
 */
function 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.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);
}

request.js

request.js 定义了 ctx.request 的原型对象的原型对象,因此该对象的任意属性都可以通过ctx.request获取。这个对象一共有30+个属性和若干方法。其中属性多数都定义了get和set方法:

module.exports = {
 get header() {
    return this.req.headers;
 },
 set header(val) {
    this.req.headers = val;
 },
 ...
}

request对象中所有的属性和方法列举如下:

属性含义属性含义header原生req对象的headersheadersheader别名url原生 req 对象的 urloriginprotocol://hosthref请求的完整urlmethod原生 req 对象的 methodpath请求 url 的 pathnamequery请求url的query,对象形式querystring请求 url 的 query ,字符串形式search?queryStringhosthosthostnamehostnameURLGet WHATWG parsed URLfresh判断缓存是否新鲜,只针对 HEAD 和 GET 方法,其余请求方法均返回falsestalefresh取反idempotent检查请求是否幂等,符合幂等性的请求有 GET , HEAD , PUT , DELETE , OPTIONS , TRACE 6个方法socket原生req对象的socketcharset请求字符集length请求的 Content-Lengthprotocol返回请求协议,https 或 http。当 app.proxy 是 true 时支持 X-Forwarded-Protosecure判断是否https请求ips当 X-Forwarded-For 存在并且 app.proxy 被启用时,这些 ips的数组被返回,从上游到下游排序。 禁用时返回一个空数组。ip请求远程地址。 当 app.proxy 是 true 时支持 X-Forwarded-Protosubdomains根据app.subdomainOffset设置的偏移量,将子域返回为数组acceptGet/Set accept objectaccepts检查给定的 type(s) 是否可以接受,如果 true,返回最佳匹配,否则为 falseacceptsEncodings(…args)检查 encodings 是否可以接受,返回最佳匹配为 true,否则为 falseacceptsCharsets(…args)检查 charsets 是否可以接受,在 true 时返回最佳匹配,否则为 false。acceptsLanguages(…args)检查 langs 是否可以接受,如果为 true,返回最佳匹配,否则为 false。is(type, …types)type()get(field)Return request header[util.inspect.custom]

response.js

response.js 定义了 ctx.response 的原型对象的原型对象,因此该对象的任意属性都可以通过ctx.response获取。和request类似,response的属性多数也定义了get和set方法。response的属性和方法如下:

属性含义属性含义socket原生res对象的socketheader原生res对象的headersheadersheader别名status响应状态码, 原生res对象的statusCodemessage响应的状态消息, 默认情况下,response.message 与 response.status 关联body响应体,支持string、buffer、stream、jsonlengthSet Content-Length field to `n`. /Return parsed response Content-Length when present.headerSent检查是否已经发送了一个响应头, 用于查看客户端是否可能会收到错误通知varyVary on fieldredirect(url, alt)执行重定向attachment(filename, options)将 Content-Disposition 设置为 “附件” 以指示客户端提示下载。(可选)指定下载的 filenametypeSet Content-Type response header with type through mime.lookup() when it does not contain a charset.lastModifiedSet/Get the Last-Modified date using a string or a Date.etagSet/Get the ETag of a response.is(type, …types)get(field)has(field)set(field, val)append(field, val)Append additional header field with value val .remove(field)Remove header field .writableChecks if the request is writable.

context.js

context.js 定义了ctx的原型对象的原型对象, 因此这个对象中所有属性都可以通过ctx访问到。context的属性和方法如下:

  • cookies 服务端cookies设置/获取操作
  • throw() 抛出包含 .status 属性的错误,默认为 500。该方法可以让 Koa 准确的响应处理状态。
  • delegate 用来将 ctx.request 和 ctx.response 两个对象上指定属性代理到ctx对象下面。这样可以直接通过 ctx.xxx 来访问ctx.request和ctx.response 对象下的属性或方法。

compose

compose来自koa-compose这个npm包,核心代码如下:

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, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

函数接收一个 middleware 数组为参数,返回一个函数,给函数传入 ctx 时第一个中间件将自动执行,以后的中间件只有在手动调用 next ,即dispatch时才会执行。另外从代码中可以看出,中间件的执行是异步的,并且中间件执行完毕后返回的是一个Promise,每个dispatch的返回值也是一个Promise,因此我们的中间件中可以方便地使用 async 函数进行定义,内部使用await next()调用“下游”,然后控制流回“上游”,这是更准确也更友好的中间件模型(洋葱模型)。

洋葱模型

洋葱模型

洋葱模型是一种中间件流程控制方式,koa2中的中间件调用就是采用了这一模型来实现的,简单代码示例如下:

const m1 = (ctx, next) => {
    ctx.req.user = null;
    console.log('中间件1 进入', ctx.req);
    next()
    console.log('中间件1 退出', ctx.req);
}

const m2 = (ctx, next) => {
    ctx.req.user = { id: 1 };
    console.log('中间件2 进入');
    next()
    console.log('中间件2 退出');
}

const m3 = (ctx, next) => {
    console.log('中间件3');
}

const middlewares = [m1, m2, m3];
const context = { req: {}, res: {} };

function dispatch(i) {
    if (i === middlewares.length) return;
    return middlewares[i](context, () => dispatch(i + 1));
}

dispatch(0);

如果这篇文章对你有帮助的话,记得关注私信我免费领取前端学习资料,观看直播课噢!(私信方法:点击我头像进我主页右上面有个私信按钮)

相关推荐

十分钟让你学会LNMP架构负载均衡(impala负载均衡)

业务架构、应用架构、数据架构和技术架构一、几个基本概念1、pv值pv值(pageviews):页面的浏览量概念:一个网站的所有页面,在一天内,被浏览的总次数。(大型网站通常是上千万的级别)2、u...

AGV仓储机器人调度系统架构(agv物流机器人)

系统架构层次划分采用分层模块化设计,分为以下五层:1.1用户接口层功能:提供人机交互界面(Web/桌面端),支持任务下发、实时监控、数据可视化和报警管理。模块:任务管理面板:接收订单(如拣货、...

远程热部署在美团的落地实践(远程热点是什么意思)

Sonic是美团内部研发设计的一款用于热部署的IDEA插件,本文其实现原理及落地的一些技术细节。在阅读本文之前,建议大家先熟悉一下Spring源码、SpringMVC源码、SpringBoot...

springboot搭建xxl-job(分布式任务调度系统)

一、部署xxl-job服务端下载xxl-job源码:https://gitee.com/xuxueli0323/xxl-job二、导入项目、创建xxl_job数据库、修改配置文件为自己的数据库三、启动...

大模型:使用vLLM和Ray分布式部署推理应用

一、vLLM:面向大模型的高效推理框架1.核心特点专为推理优化:专注于大模型(如GPT-3、LLaMA)的高吞吐量、低延迟推理。关键技术:PagedAttention:类似操作系统内存分页管理,将K...

国产开源之光【分布式工作流调度系统】:DolphinScheduler

DolphinScheduler是一个开源的分布式工作流调度系统,旨在帮助用户以可靠、高效和可扩展的方式管理和调度大规模的数据处理工作流。它支持以图形化方式定义和管理工作流,提供了丰富的调度功能和监控...

简单可靠高效的分布式任务队列系统

#记录我的2024#大家好,又见面了,我是GitHub精选君!背景介绍在系统访问量逐渐增大,高并发、分布式系统成为了企业技术架构升级的必由之路。在这样的背景下,异步任务队列扮演着至关重要的角色,...

虚拟服务器之间如何分布式运行?(虚拟服务器部署)

  在云计算和虚拟化技术快速发展的今天,传统“单机单任务”的服务器架构早已难以满足现代业务对高并发、高可用、弹性伸缩和容错容灾的严苛要求。分布式系统应运而生,并成为支撑各类互联网平台、企业信息系统和A...

一文掌握 XXL-Job 的 6 大核心组件

XXL-Job是一个分布式任务调度平台,其核心组件主要包括以下部分,各组件相互协作实现高效的任务调度与管理:1.调度注册中心(RegistryCenter)作用:负责管理调度器(Schedule...

京东大佬问我,SpringBoot中如何做延迟队列?单机与分布式如何做?

京东大佬问我,SpringBoot中如何做延迟队列?单机如何做?分布式如何做呢?并给出案例与代码分析。嗯,用户问的是在SpringBoot中如何实现延迟队列,单机和分布式环境下分别怎么做。这个问题其实...

企业级项目组件选型(一)分布式任务调度平台

官网地址:https://www.xuxueli.com/xxl-job/能力介绍架构图安全性为提升系统安全性,调度中心和执行器进行安全性校验,双方AccessToken匹配才允许通讯;调度中心和执...

python多进程的分布式任务调度应用场景及示例

多进程的分布式任务调度可以应用于以下场景:分布式爬虫:importmultiprocessingimportrequestsdefcrawl(url):response=re...

SpringBoot整合ElasticJob实现分布式任务调度

介绍ElasticJob是面向互联网生态和海量任务的分布式调度解决方案,由两个相互独立的子项目ElasticJob-Lite和ElasticJob-Cloud组成。它通过弹性调度、资源管控、...

分布式可视化 DAG 任务调度系统 Taier 的整体流程分析

Taier作为袋鼠云的开源项目之一,是一个分布式可视化的DAG任务调度系统。旨在降低ETL开发成本,提高大数据平台稳定性,让大数据开发人员可以在Taier直接进行业务逻辑的开发,而不用关...

SpringBoot任务调度:@Scheduled与TaskExecutor全面解析

一、任务调度基础概念1.1什么是任务调度任务调度是指按照预定的时间计划或特定条件自动执行任务的过程。在现代应用开发中,任务调度扮演着至关重要的角色,它使得开发者能够自动化处理周期性任务、定时任务和异...

取消回复欢迎 发表评论: