源码解析「Koa」(源码分析软件)
ccwgpt 2024-09-21 13:36 27 浏览 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);
如果这篇文章对你有帮助的话,记得关注私信我免费领取前端学习资料,观看直播课噢!(私信方法:点击我头像进我主页右上面有个私信按钮)
相关推荐
- 迈向群体智能 | 智源发布首个跨本体具身大小脑协作框架
-
允中发自凹非寺量子位|公众号QbitAI3月29日,智源研究院在2025中关村论坛“未来人工智能先锋论坛”上发布首个跨本体具身大小脑协作框架RoboOS与开源具身大脑RoboBrain,可实...
- 大模型对接微信个人号,极空间部署AstrBot机器人,万事不求百度
-
「亲爱的粉丝朋友们好啊!今天熊猫又来介绍好玩有趣的Docker项目了,喜欢的记得点个关注哦!」引言前两天熊猫发过一篇关于如何在极空间部署AstrBot并对接QQ消息平台的文章,不过其实QQ现在已经很少...
- Seata,让分布式事务不再是难题!实战分享带你领略Seata的魅力!
-
终身学习、乐于分享、共同成长!前言Seata是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata将为用户提供了AT、TCC、SAGA和XA事务模式,为用户打造一站式的...
- 常见分布式事务解决方案(分布式事务解决的问题)
-
1.两阶段提交(2PC)原理:分为准备阶段(协调者询问参与者是否可提交)和提交阶段(协调者根据参与者反馈决定提交或回滚)。优点:强一致性,适用于数据库层(如XA协议)。缺点:同步阻塞:所有参与者阻塞...
- 分布式事务:从崩溃到高可用,程序员必须掌握的实战方案!
-
“支付成功,但订单状态未更新!”、“库存扣减后,交易却回滚了!”——如果你在分布式系统中踩过这些“天坑”,这篇文章就是你的救命稻草!本文将手把手拆解分布式事务的核心痛点和6大主流解决方案,用代码实战+...
- 谈谈对分布式事务的一点理解和解决方案
-
分布式事务首先,做系统拆分的时候几乎都会遇到分布式事务的问题,一个仿真的案例如下:项目初期,由于用户体量不大,订单模块和钱包模块共库共应用(大war包时代),模块调用可以简化为本地事务操作,这样做只要...
- 一篇教你通过Seata解决分布式事务问题
-
1 Seata介绍Seata是由阿里中间件团队发起的开源分布式事务框架项目,依赖支持本地ACID事务的关系型数据库,可以高效并且对业务0侵入的方式解决微服务场景下面临的分布式事务问题,目前提供AT...
- Seata分布式事务详解(原理流程及4种模式)
-
Seata分布式事务是SpringCloudAlibaba的核心组件,也是构建分布式的基石,下面我就全面来详解Seata@mikechen本篇已收于mikechen原创超30万字《阿里架构师进阶专题合...
- 分布式事务最终一致性解决方案有哪些?MQ、TCC、saga如何实现?
-
JTA方案适用于单体架构多数据源时实现分布式事务,但对于微服务间的分布式事务就无能为力了,我们需要使用其他的方案实现分布式事务。1、本地消息表本地消息表的核心思想是将分布式事务拆分成本地事务进行处理...
- 彻底掌握分布式事务2PC、3PC模型(分布式事务视频教程)
-
原文:https://mp.weixin.qq.com/s/_zhntxv07GEz9ktAKuj70Q作者:马龙台工作中使用最多的是本地事务,但是在对单一项目拆分为SOA、微服务之后,就会牵扯出分...
- Seata分布式事务框架关于Annotation的SAGA模式分析
-
SAGAAnnotation是ApacheSeata版本2.3.0中引入的功能,它提供了一种使用Java注解而不是传统的JSON配置或编程API来实现SAGA事务模式的声明...
- 分布式事务,原理简单,写起来全是坑
-
今天我们就一起来看下另一种模式,XA模式!其实我觉得seata中的四种不同的分布式事务模式,学完AT、TCC以及XA就够了,Saga不好玩,而且长事务本身就有很多问题,也不推荐使用。S...
- 内存空间节约利器redis的bitmap(位图)应用场景有哪些你知道吗
-
在前面我们分享过一次Redis常用数据结构和使用场景,文章对Redis基本使用做了一个简单的API说明,但是对于其中String类型中的bitmap(位图)我们需要重点说明一下,因为他的作用真的不容忽...
- 分布式事务原理详解(图文全面总结)
-
分布式事务是非常核心的分布式系统,也是大厂经常考察对象,下面我就重点详解分布式事务及原理实现@mikechen本文作者:陈睿|mikechen文章来源:mikechen.cc分布式事务分布式事务指的是...
- 大家平时天天说的分布式系统到底是什么东西?
-
目录从单块系统说起团队越来越大,业务越来越复杂分布式出现:庞大系统分而治之分布式系统所带来的技术问题一句话总结:什么是分布式系统设计和开发经验补充说明:中间件系统及大数据系统前言现在有很多Java技术...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- MVC框架 (46)
- spring框架 (46)
- 框架图 (58)
- bootstrap框架 (43)
- flask框架 (53)
- quartz框架 (51)
- abp框架 (47)
- jpa框架 (47)
- laravel框架 (46)
- express框架 (43)
- springmvc框架 (49)
- 分布式事务框架 (65)
- scrapy框架 (52)
- java框架spring (43)
- grpc框架 (55)
- orm框架有哪些 (43)
- ppt框架 (48)
- 内联框架 (52)
- winform框架 (46)
- gui框架 (44)
- cad怎么画框架 (58)
- ps怎么画框架 (47)
- ssm框架实现登录注册 (49)
- oracle字符串长度 (48)
- oracle提交事务 (47)