Koa源码学习

news2024/11/26 10:50:05

前言

koa是一个非常流行的Node.js http框架。本文我们来学习下它的使用和相关源码

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

为什么使用koa

使用koa而不直接使用Node.js的http模块

  1. 高度可定制性:koa中实现了一套中间件机制以及在koa中万物皆中间件,我们通过中间件处理请求和响应并可以按需自由添加和修改中间件,并且koa的中间件生态非常丰富。而使用http需要自己编写全部的请求处理逻辑

  2. 异步编程:koa基于async/await语法,可以让异步编程变得更加简单和优雅。而直接使用http模块,则需要使用回调函数或事件监听的方式进行异步编程,不够直观

  3. 错误处理:koa内置的错误处理机制可以很好的捕获和处理错误,让代码更加健壮和可靠。而使用http模块,则需要自己编写错误处理逻辑,容易出现漏洞

  4. 扩展性:koa内置的扩展机制可以让开发者在不改变核心代码的情况下,轻松地扩展和定制koa的功能。而使用http模块,则需要自己编写全部的扩展逻辑,不够便捷

使用

koa的使用非常简单,引入koa后只需要6行代码即可访问3000端口的http服务返回一个Hello koa

const Koa = require('koa');
const app = new Koa();

app.use(ctx => {
  ctx.body = 'Hello Koa';
});

app.listen(3000);

中间件

koa本身几乎没有封装任何进一步处理http请求的能力,而是实现了一套中间件的机制,所有的逻辑均由相关的中间件进行实现,中间件可以说是koa的灵魂

koa的中间件本质是一个函数,接收一个上下文对象(context)和一个next函数作为参数,然后对请求和响应进行处理,并将控制权传递给下一个中间件。中间件可以实现各种功能,例如路由、请求处理、错误处理等

const myMiddleware = async (ctx, next) => {
  // 处理请求
  // ...
  // 调用下一个中间件
  await next();
  // 处理响应
  // ...
}

例如我们实现一个错误处理中间件,在服务端发生任何错误时给客户端返回一个500的状态码,可以以下实现即可

const errorHandler = async (ctx, next) => {
  try {
    // 处理请求
    // ...
    // 调用下一个中间件
    await next();
    // 处理响应
    // ...
  } catch (err) {
    // 处理错误
    ctx.status = 500;
    ctx.body = err.message;
  }
}

app.use(errorHandler)

以两个最常用的中间件为例

  • koa-router

koa默认也是没有封装对于特定的请求方法进行处理的功能,像很多http中处理路由相关的逻辑则需要引入koa-router 进行使用。koa router提供了基础的路由路径处理、嵌套路由等一些基础路由能力

var Koa = require('koa');
var Router = require('koa-router');

var app = new Koa();
var router = new Router();

router.get('/', (ctx, next) => {
  // ctx.router available
});

app
  .use(router.routes())
  .use(router.allowedMethods());

koa-router的源码就不展开了,原理基本上在中间件中读取req.urlreq.method 和相关req上的一些属性进行分发到相应的路由注册的回调返回中进行处理

  • koa-body

另一个常用的功能就是将请求的请求体数据解析成js对象,方便代码进行消费
对于node原生的http服务,我们需要监听请求对象的dataend事件,在data 事件中接收二进制buffer数据,在end事件中将buffer转成字符串再序列化成js对象

const Koa = require('koa');
const bodyParser = require('koa-bodyparser');

const app = new Koa();
app.use(bodyParser());

app.use(async ctx => {
  // the parsed body will store in ctx.request.body
  // if nothing was parsed, body will be an empty object {}
  ctx.body = ctx.request.body;
});

这样对于这类请求我们通过ctx.request.body就能获取到json请求的数据,无需关心从请求流关心如何获取请求体。koa-body不止处理json类型,它还会对form、text、xml等类型做相应的处理

源码实现

koa的源码非常简洁,一共只有4个文件

application

application.js定义了Koa类,用于创建koa app对象,下面是koa类的构造函数

// ...
const Emitter = require('events')
const compose = require('koa-compose');
const http = require('http');
const context = require('./context');
const request = require('./request');
const response = require('./response');
// ...

class Koa extends Emitter {
  constructor() {
    super();
    this.middleware = [];
    this.context = Object.create(context);
    this.request = Object.create(request);
    this.response = Object.create(response);
    // constructor中其它的逻辑忽略
  }
  // ...
}

Koa类继承了Emitter类,用于实现事件的发布和订阅。还定义了一些属性,主要包括middlewarecontextrequestresponse。其中,middleware是中间件函数数组,用于存储所有的中间件函数;context是koa的请求上下文对象、request是请求对象实例、response是响应对象实例

koa实例上也暴露了几个对外使用的方法

  • app.listen

上面的使用demo,可以看到调用listen后就是监听指定端口运行起我们的http服务

通过查看app.listen 的实现本质是调用了app.callback获取到回调函数处理逻辑,再传给http.createSerever。所以也等价于以下调用

const http = require('http');
const Koa = require('koa');
const app = new Koa();
http.createServer(app.callback()).listen(3000);
  • app.callback

返回可以直接传递给 http.createServer() 方法的回调函数来处理请求

  callback () {
    const fn = this.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) {
    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)
  }

主要有以下几个逻辑

  • 判断我们是否有监听错误事件进行处理(this.listenerCount是继承的EventEmiter上用于获取某个事件监听次数的方法),如果没有则使用koa自带的默认错误处理

  • 使用回调入参的request对象和response对象构造请求上下文对象并传递给this.handleRequest函数进行处理

  • handleRequest中,就是调用了被compose完成后的中间件函数,在处理完成后调用respond进行结束整个请求的流程

  • 在koa中我们无需像Node.js中http需要显式调用res.end或者res.pipe进行响应的结束发送,因为在handleResponserespond函数中处理了。它会根据我们在业务逻辑设置的不同的body的类型进行相关调用,例如如果是一个流则调用pipe进行流式返回、特定状态码不返回body、非buffer和string的body序列化成字符串等

  • 洋葱模型

koa的洋葱模型是一种中间件处理机制其核心是将请求和响应对象传递给一系列中间件函数,每个中间件函数都可以对请求和响应进行处理,并将控制权传递给下一个中间件函数,最终将响应返回给客户端。中间件函数在请求处理过程中像是一个个套在一起的“洋葱”,请求从外层中间件函数开始处理,逐层深入,直到最内层中间件函数,然后逐层返回,最终响应从最外层中间件函数返回给客户端

在洋葱模型中,每个中间件函数都是一个异步async函数。在处理请求时,每个中间件函数都接收一个context对象和一个next函数作为参数,context对象包含了请求和响应的信息,next函数可以调用下一个中间件函数

处理顺序如下

  1. 请求从外层中间件函数开始处理,先经过第一个中间件函数

  2. 第一个中间件函数处理请求,然后调用next函数,将控制权传递给下一个中间件函数

  3. 下一个中间件函数也处理请求,然后调用next函数,将控制权传递给下一个中间件函数,直到最内层中间件函数

  4. 最内层中间件函数处理请求完成后逐层返回每个中间件函数在返回时可以对响应进行处理

  5. 最后,响应从最外层中间件函数返回给客户端

洋葱模型的优点是可以将请求和响应的处理逻辑分解成多个模块,每个模块只需关注自己的逻辑,提高了代码的可维护性。由于每个中间件函数都可以对请求和响应进行处理,因此可以实现一些复杂的功能例如身份验证、日志记录、错误处理等

主要是koa-compose包的实现将中间件函数组合在一起,compoose实现代码如下

function compose (middleware) {
  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)
      }
    }
  }
}

// 使用
 const fn = this.compose(this.middleware)

compose函数接收一个中间件函数数组作为参数,返回一个新的中间件。新的中间件函数接收contextnext对应于常规中间件的入参

函数内部实现了dispatch,用于递归调用中间件数组中的每个函数。
dispatch函数接收一个参数i,表示当前调用的中间件函数在数组中的索引。如果i小于等于上一次调用的索引index,则表示next函数被多次调用,koa中间件中next只能被调用一次,调用多次会抛出一个错误。然后,dispatchindex赋值为i,表示当前调用的中间件函数已经被执行。然后dispatch函数会从中间件数组中取出当前索引对应的函数fn,如果当前索引i等于数组长度则说明已经到达中间件函数数组的末尾然后将fn设置为next函数。如果fn不存在则直接返回一个已经resolvePromise。最后dispatch函数通过Promise.resolve调用当前中间件函数,并将dispatch.bind(null, i + 1)作为下一个中间件函数的next参数传入,以便递归调用下一个中间件函数。如果当前中间件函数抛出了一个错误则通过Promise.reject将错误传递给下一个中间件函数

总结原理是通过递归调用中间件函数数组中的每个函数,并将next函数作为参数传入,实现洋葱模型中间件的处理顺序。在递归调用的过程中,如果某个中间件函数抛出了错误则通过Promise.reject将错误逐层传递给下一个中间件函数,直到最终返回错误响应或者成功响应

context

请求上下文对象,对应中间件的ctx入参
context.js文件主要是对外导出了一个对象,以及执行了一系列delegate操作

  • 导出的对象主要是封装了cookie的读取逻辑

  • delegate方法是从delegates npm包进行导入(这个包的解读见# 每天阅读一个 npm 模块(7)- delegates - 掘金)

简单来说就是将对context对象上的操作代理到koa封装的requestresponse对象中去

// proto这里是context
delegate(proto, 'response').method('append').access('body')

这个执行后的结果就是

  • context.append方法调用实际调用的是context.response.append

  • context.body的读写实际调的是context.response.body的读写
    context.response则在下面的createContext时将koaresponse对象设置在context对象中去

application中通过createContext方法构造后传入请求处理回调函数

class Koa extends Emitter {
  constructor() {
    //....
    this.context = Object.create(context);
    //....
  }
  // ...
    callback () {
    const fn = this.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
  }
  
    createContext (req, res) {
    /** @type {Context} */
    const context = Object.create(this.context)
    /** @type {KoaRequest} */
    const request = context.request = Object.create(this.request)
    /** @type {KoaResponse} */
    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
  }
}

主要是将我们koa中常用的几个对象挂载到相应的地方,经过createContext的操作,我们可以得到可以通过以下方式获取相关对象

  • koa app实例

    • app === context.app === context.request.app === context.response.app
  • koa 请求context对象

    • context === context.request.ctx === context.response.ctx
  • koa request对象

    • ctx.request === ctx.response.request
  • koa response对象

    • ctx.response === ctx.request.response
  • 原生req对象

    • context.req === context.request.req === context.response.req
  • 原生res对象

    • context.res === context.response.res === context.request.res

request

koa中的请求对象封装。基本上都是基于Node.js的http请求的request做一些便捷使用的二次封装的属性和方法,并挂载在ctx.request

一个例子就是Node.js 的http server回调函数入参的req对象http.ImcomingMessage 是没有提供便捷的获取query参数信息,它只有一个url属性

而koa的request对象则实现了query的解析、获取、设置等

// request.js
get query () {
    const str = this.querystring
    const c = this._querycache = this._querycache || {}
    return c[str] || (c[str] = qs.parse(str))
  }
  
get querystring () {
    if (!this.req) return ''
    return parse(this.req).query || ''
  }

response

koa中的响应对象封装,基于Node.js的http请求的response做一些封装的属性和方法,挂载在ctx.response

一个比较常用到的就是会有根据我们的ctx.body设置的值(会delegate到ctx.response.body中)帮我们去设置response的Content-Type的值,例如给ctx.body设置一个普通js对象的话,会将Content-Type设置为json类型并将js对象json序列化(序列化逻辑在上面提到的respond函数中)

最近更新

作为一个代码实现非常精简且已经非常稳定的广泛使用的框架,一般来说不会有什么更新了,2.x也已经稳定了很久。但是在1/2却更新了3.0.0-alpha.0版本,翻看更新记录这个大版本目前只更新了一个功能

可以直接使用app.currentContext来获取当前的请求上下文对象,这个功能可以方便不少我们的代码开发

通过上面我们知道,koa的contxt对象是每次请求维度的一个新对象,如果我们想在一些封装的方法中获拿到当前请求的context对象,必须层层传递context对象会比较麻烦

//  fn.js
const fn = (ctx) => {
    console.log(ctx.url)
}
exports.fn = fn

// app.js
const app = new Koa();
app.use(ctx => {
  ctx.body = 'Hello Koa';
  fn(ctx)
}).listen(3000);

而支持了app.currentContext后,我们在任意地方想获取当前的请求上下文对象直接app.currentContext即可,无需再多层透传context对象

//  fn.js
const fn = () => {
    console.log(app.currentContext.url)
}

这个功能的实现利用了Node.js的async_hooks模块提供的AsyncLocalStorageAsyncLocalStorage 是 Node.js 在v14.8.0 版本中引入的一个模块,是官方推荐的在异步代码中管理数据的方式之一,会将我们保存的数据与异步操作所在的上下文关联起来,确保在异步操作中访问到相应正确的数据

AsyncLocalStorage 有两个主要的方法

  • run():用于在异步操作中保存数据。接收一个回调函数作为参数,该回调函数会在异步操作执行期间被调用,并且在该回调函数中保存的数据会与异步操作所在的上下文关联起来

  • getStore():用于在异步操作中获取数据。它会返回与异步操作所在的上下文关联的数据

所以在koa中实现app.currentContext功能主要就是以下代码

// application.js
class Application extends Emitter {
   constructor (options) {
    //....
    if (options.asyncLocalStorage) {
      const { AsyncLocalStorage } = require('async_hooks')
      this.ctxStorage = new AsyncLocalStorage()
      this.use(this.createAsyncCtxStorageMiddleware())
        }
    }
   // ...
  createAsyncCtxStorageMiddleware () {
    const app = this
    return async function asyncCtxStorage (ctx, next) {
      await app.ctxStorage.run(ctx, async () => {
        return await next()
      })
    }
  }
  // ....
   get currentContext () {
    if (this.ctxStorage) return this.ctxStorage.getStore()
  }
}
  1. 如果初始化时配置了option.asyncLocalStorage,就注册一个放在第一位的koa中间件

  2. 在请求进入中间件时会执行ctxStorage.run 存入当前的context对象并马上在回调函数中执行next(即请求后续所有的操作)

  3. 在后续获取即可通过getStore()获取到当前请求的context对象

总结

通过本文的学习我们了解到了koa的一些使用和实现,koa的源码是非常精简的没有太多耦合功能,但是设计了巧妙的中间件机制设计来方便让我们开发各种功能

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/403076.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【数据分析】Excel必备函数汇总

文章目录求和单条件求和SUMIF多条件求和SUMIFS求平均AVERAGE单条件多条件查找纵向VLOOKUP函数eg1已知身份证查姓名eg2已知身份证查地区Find函数FindB函数SEARCH函数SEARCHBMATCH函数Subtotal函数最值小数点去小数点TRUNC四舍五入ROUND随机数求余奇偶统计条件多条件时间当前日期…

LearnOpenGL-光照-5.投光物

本人刚学OpenGL不久且自学&#xff0c;文中定有代码、术语等错误&#xff0c;欢迎指正 我写的项目地址&#xff1a;https://github.com/liujianjie/LearnOpenGLProject 文章目录投光物平行光点光源聚光不平滑的例子平滑例子投光物 前面几节使用的光照都来自于空间中的一个点 即…

IR 808 Alkyne,IR-808 alkyne,IR 808炔烃,近红外吲哚类花菁染料

【产品理化指标】&#xff1a;中文名&#xff1a;IR-808炔烃英文名&#xff1a;IR-808 alkyne&#xff0c;Alkyne 808-IR CAS号&#xff1a;N/AIR-808结构式&#xff1a;规格包装&#xff1a;10mg&#xff0c;25mg&#xff0c;50mg&#xff0c;接受各种复杂PEGS定制服务&#x…

Git的下载、安装、配置、使用、卸载

前言 我是跟着狂神老师学的。该博客仅用于笔记所用。 下面是老师的B站和笔记 B站&#xff1a;https://www.bilibili.com/video/BV1FE411P7B3?p1&vd_source9266cf72b1f398b63abe0aefe358d7d6 笔记&#xff1a;https://mp.weixin.qq.com/s/Bf7uVhGiu47uOELjmC5uXQ 一、准备工…

【18】组合逻辑 - VL18 实现3-8译码器①

VL18 实现3-8译码器① 1 题目 【这题我的思路非常绝境】奈斯 !! 看真值表的思路:Yi所在列【0仅一个其余全1】,故【以0为对象求解】 观察发现:E3 E2_n E1_n = 100 时 是 译码的使能信号 ; 并且E3 E2_n E1_n为其他值时,都不使能译码 然后就很简单,没有仿真就成功了 2 代…

Linux:文件流指针 与 文件描述符

目录一、文件描述符二、文件流指针三、缓冲区之前讲解过了IO库函数和IO接口&#xff0c;库函数是对系统调用接口的封装&#xff0c;也就是说实际上在库函数内部是通过调用系统调用接口来完成最终功能的。 库函数通过文件流指针操作文件&#xff0c;系统调用接口通过文件描述符操…

在一个web应用中应该如何完成资源的跳转

在一个web应用中通过两种方式&#xff0c;可以完成资源的跳转&#xff1a; 第一种方式&#xff1a;请求转发 第二种方式&#xff1a;重定向 转发和重定向的区别&#xff1a; 代码上的区别&#xff1a; 请求转发 // 获取请求转发器对象 RequestDispatcher dispatcher request.…

3-1 SpringCloud快速开发入门: Ribbon 是什么

接上一章节Eureka 服务注册中心自我保护机制&#xff0c;这里讲讲Ribbon 是什么 Ribbon 是什么 通常说的负载均衡是指将一个请求均匀地分摊到不同的节点单元上执行&#xff0c;负载均和分为硬件负载均衡和软件负载均衡&#xff1a; **硬件负载均衡&#xff1a;**比如 F5、深信…

C# 实现 key-value 结构自定义缓存 CustomCache

功能需求 使用 C# 编写一个 key-value 结构进程内缓存&#xff0c;实现数据的缓存操作&#xff0c;此处所用到的知识点如下&#xff1a; 线程安全的字典 ConcurrentDictionary&#xff1b;设计模式之单例模式&#xff08;Singleton&#xff09;&#xff1b;缓存数据【主动 &a…

3.10多线程

一.常见锁策略1.悲观锁 vs乐观锁体现在处理锁冲突的态度①悲观锁:预期锁冲突的概率高所以做的工作更多,付出的成本更多,更低效②乐观锁:预期锁冲突的概率低所以做的工作少,付出的成本更低,更搞笑2.读写锁 vs 普通的互斥锁①普通的互斥锁,只有两个操作 加锁和解锁只有两个线程针…

HT32合泰单片机开发环境搭建和配置教程

HT32合泰(Holtek)单片机开发环境搭建安装教程 前言 最近在准备合泰杯的比赛&#xff0c;在看合泰官方的PPT和数据手册学习&#xff0c;顺便做个合泰单片机的开发环境搭建教程。 合泰杯比赛发放的开发板是ESK32-30501&#xff0c;用的单片机是HT32F52352。 合泰杯官网地址&a…

【C++】vector的使用及其模拟实现

这里写目录标题一、vector的介绍及使用1. vector的介绍2. 构造函数3. 遍历方式4. 容量操作及空间增长问题5. 增删查改6. vector二维数组二、vector的模拟实现1. 构造函数2. 迭代器和基本接口3. reserve和resize4. push_back和pop_back5. insert和erase5. 迭代器失效问题5. 浅拷…

Java中的 this 和 super

1 this 关键字 1.1 this 访问本类属性 this 代表对当前对象的一个引用 所谓当前对象&#xff0c;指的是调用当前类中方法或属性的那个对象this只能在方法内部使用&#xff0c;表示对“调用方法的那个对象”的引用this.属性名&#xff0c;表示本对象自己的属性 当对象的属性和…

IntelliJ IDEA 编码设置

1.场景 适用于配置idea文件编码 2.配置 对已经存在的项目设置文件编码 可以设置全局的编码 以及 项目的编码 一般没啥特殊要求 都建议设置为 UTF-8 以及 配置项目的目录的单独编码 也建议UTF-8 idea可以单独设置properties的编码 也建议改为 UTF-8&#xff0c;其中有一个重点…

HCIP --- GRE和MGRE

VPN----虚拟私有网络&#xff1a;依靠ISP或者其他网络管理机构在公有网络基础上构建的专用的安全数据通信网络&#xff0c;只不过该网络是逻辑上的而非物理的。 虚拟&#xff1a;用户不再需要拥有实际的长途数据线路&#xff0c;而是使用公共网络资源建立的属于自己的私有网络…

[论文笔记]Transformer-XL: Attentive Language Models Beyond a Fixed-Length Context

引言 我们知道Transformer很好用&#xff0c;但它设定的最长长度是512。像一篇文章超过512个token是很容易的&#xff0c;那么我们在处理这种长文本的情况下也想利用Transformer的强大表达能力需要怎么做呢&#xff1f; 本文就带来一种处理长文本的Transformer变种——Transf…

SQS (Simple Queue Service)简介

mazon Simple Queue Service (SQS)是一种完全托管的消息队列服务&#xff0c;可以让你分离和扩展微服务、分布式系统和无服务应用程序。 在讲解SQS之前&#xff0c;首先让我们了解一下什么是消息队列。 消息队列 还是举一个电商的例子&#xff0c;一个用户在电商网站下单后付…

【LeetCode每日一题:[面试题 17.05] 字母与数字-前缀和+Hash表】

题目描述 给定一个放有字母和数字的数组&#xff0c;找到最长的子数组&#xff0c;且包含的字母和数字的个数相同。 返回该子数组&#xff0c;若存在多个最长子数组&#xff0c;返回左端点下标值最小的子数组。若不存在这样的数组&#xff0c;返回一个空数组。 示例 1: 输入…

LDGRB-01 3AFE61320954P0001

LDGRB-01 3AFE61320954P0001变频器的作用_变频器工作原理变频器是把工频电源&#xff08;50Hz或60Hz&#xff09;变换成各种频率的交流电源&#xff0c;以实现电机的变速运行的设备&#xff0c;其中控制电路完成对主电路的控制&#xff0c;整流电路将交流电变换成直流电&#x…

教育培训机构屡遭投诉?湖南中创教育给出三点建议

在中消协发布的《2021年校外教育培训领域消费者权益保护报告》中&#xff0c;受新冠疫情以及校外教育培训行业治理政策的冲击&#xff0c;我国校外教育培训领域消费维权舆情及消费者投诉呈上升趋势。2021年全国消协组织受理有关校外教育培训的投诉案件共80,528件&#xff0c;同…