从浅入深了解Koa2源码(koa源码解析)
ccwgpt 2024-09-21 13:37 30 浏览 0 评论
在前文我们介绍过什么是 Koa2 的基础
简单回顾下
什么是 koa2
- NodeJS 的 web 开发框架
- Koa 可被视为 nodejs 的 HTTP 模块的抽象
源码重点
中间件机制
洋葱模型
compose
源码结构
Koa2 的源码地址:https://github.com/koajs/koa
其中 lib 为其源码
koa2源码
可以看出,只有四个文件:application.js、context.js、request.js、response.js
application
为入口文件,它继承了 Emitter 模块,Emitter 模块是 NodeJS 原生的模块,简单来说,Emitter 模块能实现事件监听和事件触发能力
application1
删掉注释,从整理看 Application 构造函数
Application构造函数
Application 在其原型上提供了 listen、toJSON、inspect、use、callback、handleRequest、createContext、onerror 等八个方法,其中
- listen:提供 HTTP 服务
- use:中间件挂载
- callback:获取 http server 所需要的 callback 函数
- handleRequest:处理请求体
- createContext:构造 ctx,合并 node 的 req、res,构造 Koa 的 参数——ctx
- onerror:错误处理
其他的先不要在意,我们再来看看 构造器 constructor
Application的构造器
晕,这都啥和啥,我们启动一个最简单的服务,看看实例
const Koa = require('Koa')
const app = new Koa()
app.use((ctx) => {
ctx.body = 'hello world'
})
app.listen(3000, () => {
console.log('3000请求成功')
})
console.dir(app)
实例
能看出来,我们的实例和构造器一一对应,
打断点看原型
断点
哦了,除去非关键字段,我们只关注重点
Koa 的构造器上的 this.middleware、 this.context、 this.request、this.response
原型上有:listen、use、callback、handleRequest、createContext、onerror
注:以下代码都是删除异常和非关键代码
先看 listen
...
listen(...args) {
const server = http.createServer(this.callback())
return server.listen(...args)
}
...
可以看出 listen 就是用 http 模块封装了一个 http 服务,重点是传入的 this.callback()。好,我们现在就去看 callback 方法
callback
callback() {
const fn = compose(this.middleware)
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res)
return this.handleRequest(ctx, fn)
}
return handleRequest
}
它包含了中间件的合并,上下文的处理,以及 res 的特殊处理
中间件的合并
使用了 koa-compose 来合并中间件,这也是洋葱模型的关键,koa-compose 的源码地址:https://github.com/koajs/compose。这代码已经三年没动了,稳的一逼
function compose(middleware) {
return function (context, next) {
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 是什么,即中间件数组,那它是怎么来的呢,构造器中有 this.middleware,谁使用到了—— use 方法
我们先跳出去先看 use 方法
use
use(fn) {
this.middleware.push(fn)
return this
}
除去异常处理,关键是这两步,this.middleware 是一个数组,第一步往 this.middleware 中 push 中间件;第二步返回 this 让其可以链式调用,当初本人被面试如何做 promise 的链式调用,懵逼脸,没想到在这里看到了
回过头来看 koa-compose 源码,设想一下这种场景
...
app.use(async (ctx, next) => {
console.log(1);
await next();
console.log(6);
});
app.use(async (ctx, next) => {
console.log(2);
await next();
console.log(5);
});
app.use(async (ctx, next) => {
console.log(3);
ctx.body = "hello world";
console.log(4);
});
...
我们知道 它的运行是 123456
它的 this.middleware 的构成是
this.middleware = [
async (ctx, next) => {
console.log(1)
await next()
console.log(6)
},
async (ctx, next) => {
console.log(2)
await next()
console.log(5)
},
async (ctx, next) => {
console.log(3)
ctx.body = 'hello world'
console.log(4)
},
]
不要感到奇怪,函数也是对象之一,是对象就可以传值
const fn = compose(this.middleware)
我们将其 JavaScript 化,其他不用改,只需要把最后一个函数改成
async (ctx, next) => {
console.log(3);
-ctx.body = 'hello world';
+console.log('hello world');
console.log(4);
}
测试compose
测试compose2
逐行解析 koa-compose
这一段很重要,面试的时候常考,让你手写一个 compose ,淦它
//1. async (ctx, next) => { console.log(1); await next(); console.log(6); } 中间件
//2. const fn = compose(this.middleware) 合并中间件
//3. fn() 执行中间件
function compose(middleware) {
return function (context, next) {
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 = compose(this.middleware),即如下代码
const fn = function (context, next) {
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)
}
}
}
}
执行 fn(),即如下代码:
const fn = function (context, next) {
let index = -1
return dispatch(0)
function dispatch (i) {
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i // index = 0
let fn = middleware[i] // fn 为第一个中间件
if (i === middleware.length) fn = next // 当弄到最后一个中间件时,最后一个中间件赋值为 fn
if (!fn) return Promise.resolve()
try {
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
// 返回一个 Promise 实例,执行 递归执行 dispatch(1)
} catch (err) {
return Promise.reject(err)
}
}
}
}
也就是第一个中间件,要先等第二个中间件执行完才返回,第二个要等第三个执行完才返回,直到中间件执行执行完毕
Promise.resolve 就是个 Promise 实例,之所以使用 Promise.resolve 是为了解决异步,之所以使用 Promise.resolve 是为了解决异步
抛去 Promise.resolve,我们先看一下递归的使用,执行以下代码
const fn = function () {
return dispatch(0);
function dispatch(i) {
if (i > 3) return;
i++;
console.log(i);
return dispatch(i++);
}
};
fn(); // 1,2,3,4
回过头来再看一次 compose,代码类似于
// 假设 this.middleware = [fn1, fn2, fn3]
function fn(context, next) {
if (i === middleware.length) fn = next // fn3 没有 next
if (!fn) return Promise.resolve() // 因为 fn 为空,执行这一行
function dispatch (0) {
return Promise.resolve(fn(context, function dispatch(1) {
return Promise.resolve(fn(context, function dispatch(2) {
return Promise.resolve()
}))
}))
}
}
}
这种递归的方式类似执行栈,先进先出
执行栈
这里要多思考一下,递归的使用,对 Promise.resolve 不用太在意
上下文的处理
上下文的处理即调用了 createContext
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
}
传入原生的 request 和 response,返回一个 上下文——context,代码很清晰,不解释
res 的特殊处理
callback 中是先执行 this.createContext,拿到上下文后,再去执行 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)
}
一切都清晰了
const Koa = require('Koa');
const app = new Koa();
console.log('app', app);
app.use((ctx, next) => {
ctx.body = 'hello world';
});
app.listen(3000, () => {
console.log('3000请求成功');
});
这样一段代码,实例化后,获得了 this.middleware、this.context、this.request、this.response 四大将,你使用 app.use() 时,将其中的函数推到 this.middleware。再使用 app.listen() 时,相当于起了一个 HTTP 服务,它合并了中间件,获取了上下文,并对 res 进行了特殊处理
错误处理
onerror(err) {
if (!(err instanceof Error))
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()
console.error(msg.replace(/^/gm, ' '))
console.error()
}
context.js
引入我眼帘的是两个东西
// 1.
const proto = module.exports = {
inspect(){...},
toJSON(){...},
...
}
// 2.
delegate(proto, 'response')
.method('attachment')
.access('status')
...
第一个可以理解为,const proto = { inspect() {...} ...},并且 module.exports 导出这个对象
第二个可以这么看,delegate 就是代理,这是为了方便开发者而设计的
// 将内部对象 response 的属性,委托至暴露在外的 proto 上
delegate(proto, 'response')
.method('redirect')
.method('vary')
.access('status')
.access('body')
.getter('headerSent')
.getter('writable');
...
而使用 delegate(proto, 'response').access('status')...,就是在 context.js 导出的文件,把 proto.response 上的各个参数都代理到 proto 上,那 proto.response 是什么?就是 context.response,context.response 哪来的?
回顾一下, 在 createContext 中
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.response 有了,就明了了, context.response = this.response,因为 delegate,所以 context.response 上的参数代理到了 context 上了,举个例子
- ctx.header 是 ctx.request.header 上代理的
- ctx.body 是 ctx.response.body 上代理的
request.js 和 response.js
一个处理请求对象,一个处理返回对象,基本上是对原生 req、res 的简化处理,大量使用了 ES6 中的 get 和 post 语法
大概就是这样,了解了这么多,怎么手写一个 Koa2 呢,请看下一篇——手写 Koa2
参考资料
- KOA2 框架原理解析和实现
- 可能是目前最全的 koa 源码解析指南
相关推荐
- 十分钟让你学会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什么是任务调度任务调度是指按照预定的时间计划或特定条件自动执行任务的过程。在现代应用开发中,任务调度扮演着至关重要的角色,它使得开发者能够自动化处理周期性任务、定时任务和异...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- MVC框架 (46)
- spring框架 (46)
- 框架图 (58)
- flask框架 (53)
- quartz框架 (51)
- abp框架 (47)
- jpa框架 (47)
- laravel框架 (46)
- springmvc框架 (49)
- 分布式事务框架 (65)
- scrapy框架 (56)
- shiro框架 (61)
- 定时任务框架 (56)
- java日志框架 (61)
- JAVA集合框架 (47)
- grpc框架 (55)
- ppt框架 (48)
- 内联框架 (52)
- winform框架 (46)
- gui框架 (44)
- cad怎么画框架 (58)
- ps怎么画框架 (47)
- ssm框架实现登录注册 (49)
- oracle字符串长度 (48)
- oracle提交事务 (47)