Koa源码解析
Koa是一款设计优雅的轻量级Node.js框架,它主要提供了一套巧妙的中间件机制与简练的API封装,因此源码阅读起来也十分轻松,不论你从事前端或是后端研发,相信都会有所收获。
目录结构
首先将源码下载到本地,可以看到Koa的源码只包含下述四个文件:
lib
├── application.js
├── context.js
├── request.js
└── response.js
application.js
application.js为Koa的主程序入口文件,在package.json的main字段有定义。它主要负责HTTP服务的注册、封装请求相应对象,并初始化中间件数组并通过compose方法进行执行。
context.js
context.js的核心工作为将请求与响应方法集成到一个上下文(Context)中,上下文中的大多数方法都是直接委托到了请求与响应对象上,本身并没做什么改变,它能为编写Web应用程序提供便捷。
request.js
request.js将http库的request方法进行抽象与封装,通过它可以访问到各种请求信息。
response.js
response.js与request功能类似,它是对response对象的抽象与封装。
中间件
示例
对于Koa的中间件机制相信大家都耳熟能详了,现在让我们来看看源码实现。在这里还是先举一个最简单的例子:
const Koa = require('koa');
const app = new Koa();
app.use((ctx, next) => {
console.log('enter 1');
next();
console.log('out 1');
});
app.use((ctx, next) => {
console.log('enter 2');
next();
console.log('out 2');
});
app.use((ctx, next) => {
console.log('enter 3');
next();
console.log('out 3');
});
app.listen(3000);
现在让我们来访问应用:curl 127.0.0.1:3000
,可以看到以下输出结果:
enter 1
enter 2
enter 3
out 3
out 2
out 1
next是什么?
通过以上的结果进行分析,当我们执行next()
的时候,可能程序的执行权交给了下一个中间件,next函数会等待下一个中间件执行完毕,然后接着执行,这样的执行机制被称为“洋葱模型”,因为它就像请求穿过一层洋葱一样,先从外向内一层一层执行,再从内向外一层一层返回,而next就是进行下一层的一把钥匙:
原理
聊完了理想,现在我们来聊现实。首先来看看app.use函数:
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;
}
整个函数只做了一件事情,将中间件函数添加到了实例中的middleware数组,其他的即是对类型进行校验,若不为函数则直接报TypeError,若为生成器则发出deprecated警告并使用koa-convert[注1]对其转化。
中间件在什么时候执行的呢?首先我们找到listen的回调函数:
const server = http.createServer(this.callback());
然后来看看这个神奇的callback函数:
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;
}
函数首先将中间件使用koa-compose进行处理,那个compose到底是个什么呢?不如直接来看源码吧(省略掉了注释与类型检测):
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)
}
}
}
}
首先我们把目光放到index
与i
两个变量上,当执行执行compose(middleware)
函数时,会返回一个闭包函数distpach(0)
,闭包函数执行时,dispatch函数内部的判断逻辑如下:
- 若i小于等于index 则报出错误:'next() called multiple times'。
- 若i大于index时,将i赋予index,此时i与index相等。
逻辑很简单,但这样做的目的是什么呢?假若程序按着预期执行,每个中间件内部都执行next(),假若有3个中间件,那么当每次执行dispatch(i)时,到Line8之前index与i的值分别为:-1/0, 0/1, 1/2
,可以看出i始终要大于index,index的闭包变量每次在执行完函数后都会加1,因此可以知道的是若同一个中间件执行了两次,index就会等于i,再执行一次index就会大于i,由此可知,index的存在意义在于限制next能执行不超过1次。
Line9到Line11用于取出middleware中的当前中间件,若数组为最大索引标识,则会将fn等于next函数,意味着将再执行一次越级的索引i + 1
,由于取不到值,于是就执行到Line11返回Promise.resolve()。
当函数执行到Line13,则会运行当前中间件,并将是否执行下一个中间件dispatch(i + 1)的决定权传递到next参数,将运行结果返回,返回函数的运行结果的意义在于每次执行next的返回结果都是下一个中间件的执行结果的Promise对象。
回到callback
让我们继续看callback函数等剩余逻辑:
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;
}
首先来看看Line3,因为Application继承与Emitter,故此方法是用于监听实例中的error事件的,当listenerCount的数值为0时,表示没有监听过,则注册监听函数。
接着生成一个handleRequest回调,当每个请求过来时,都会创建ctx上下文对象,并将中间件函数传入实例方法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);
}
在这里多出了几个函数:
- on-finished,监听请求是否正常结束。
respond
, 当中间件执行完毕后,处理response对象的status与body的字段。
回到示例
回到示例,是否恍如隔日?现在的代码还困扰你吗?让我们稍作修改:
app.use((ctx, next) => {
console.log('enter 1');
next();
console.log('out 1');
});
app.use((ctx, next) => {
console.log('enter 2');
});
app.use((ctx, next) => {
console.log('enter 3');
next();
console.log('out 3');
});
此时你能准确的知道执行结果吗?此时打印顺序为:enter 1 -> enter 2 -> out 1
。因为只有next才是进入到下一中间件的钥匙。若再将程序改一改:
app.use(async (ctx, next) => {
console.log('enter 1');
next();
console.log('out 1');
});
app.use(async (ctx, next) => {
console.log('enter 2');
await next();
console.log('out 2');
});
此时执行结果为:enter1 -> enter2 -> out 1 -> out2
,这你能答对吗?你不需要记住范式与结果,回想一下核心的compose函数:return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
,首先中间件全为async函数,若使用await next(),则会等待下一个中间件返回resolve状态才会执行此代码,如果某一个Promise中间件不使用await关键字呢?它会在主进程上进行排队等待,等到函数执行栈返回到当前函数后立即执行。对于此示例来讲,当进入到第二个中间件,遇到await关键字时,console.log('out 2')
则不会再执行,而是进入到微任务队列中,此时主进程已无其他任务,则函数退出当前栈,返回到了第一个函数中,此时输出out 1,当第一个中间件执行结束后,事件循环才会将中间件2的微任务取出来执行,因此你见到了上述的输出顺序。
上下文
通过上述分析,我们了解到http.createServer中有一个callback函数,它不仅负责执行compose函数,也会调用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;
}
可以由这个函数得知,ctx对象包含了`context.js
request.js、response.js`的代码。通过访问req与res的源代码,大家可以发现在request与response对象中封装了许许多多http库的请求方法与各类工具函数,若对http底层实现感兴趣的小伙伴可以仔细读一下request与response文件,否则多查阅几遍官网文档,大概了解其中的api即可。
而对于context.js,其实十分简单,它也封装了部分工具方法,并使用node-delegates进行委托方法与属性,对于此类方法的时间,估计koa3会将这一部分进行重构为Proxy吧。
注解
1. koa-convert转化
在Koa版本号为1.x时,中间件都是使用Generator实现的,因此可以通过官方提供的koa-convert临时对其进行转化与兼容,基本用法为:
function * legacyMiddleware (next) {
// before
yield next
// after
}
app.use(convert(legacyMiddleware))
然后打开源码发现,核心代码大概如下:
function convert (mw) {
return (ctx, next) => co.call(ctx, mw.call(ctx, createGenerator(next)))
}
convert函数将生成器通过co进行包装为Promise函数,在ctx上下文进行执行,并传入next函数。
总结
凡是涉及到原理性的东西,感觉自己很难避免自顾自说,用图片进行可视化的方式会更加直观,易于理解,希望之后自己多多使用图片来阐述原理。
通过源码分析,我们知道了Koa的核心思想建立于中间件机制,它是一个设计十分简洁、巧妙的Web框架,扩展性极强,egg.js就是建立于Koa之上的上层框架。
来源:https://segmentfault.com/a/1190000018202746
Koa源码解析的更多相关文章
- Koa源码解析,带你实现一个迷你版的Koa
前言 本文是我在阅读 Koa 源码后,并实现迷你版 Koa 的过程.如果你使用过 Koa 但不知道内部的原理,我想这篇文章应该能够帮助到你,实现一个迷你版的 Koa 不会很难. 本文会循序渐进的解析内 ...
- Node.js躬行记(19)——KOA源码分析(上)
本次分析的KOA版本是2.13.1,它非常轻量,诸如路由.模板等功能默认都不提供,需要自己引入相关的中间件. 源码的目录结构比较简单,主要分为3部分,__tests__,lib和docs,从名称中就可 ...
- Koa2 源码解析(1)
Koa2 源码解析 其实本来不想写这个系列文章的,因为Koa本身很精简,一共就4个文件,千十来行代码. 但是因为想写 egg[1] 的源码解析,而egg是基于Koa2的,所以就先写个Koa2的吧,用作 ...
- Koa源码分析(二) -- co的实现
Abstract 本系列是关于Koa框架的文章,目前关注版本是Koa v1.主要分为以下几个方面: Koa源码分析(一) -- generator Koa源码分析(二) -- co的实现 Koa源码分 ...
- Koa源码分析(一) -- generator
Abstract 本系列是关于Koa框架的文章,目前关注版本是Koa v1.主要分为以下几个方面: 1. Koa源码分析(一) -- generator 2. Koa源码分析(二) -- co的实现 ...
- koa源码阅读[3]-koa-send与它的衍生(static)
koa源码阅读的第四篇,涉及到向接口请求方提供文件数据. 第一篇:koa源码阅读-0第二篇:koa源码阅读-1-koa与koa-compose第三篇:koa源码阅读-2-koa-router 处理静态 ...
- 新一代web框架Koa源码学习
此文已由作者张佃鹏授权网易云社区发布. 欢迎访问网易云社区,了解更多网易技术产品运营经验. Koa 就是一种简单好用的 Web 框架.它的特点是优雅.简洁.表达力强.自由度高.本身代码只有1000多行 ...
- 【原】Android热更新开源项目Tinker源码解析系列之三:so热更新
本系列将从以下三个方面对Tinker进行源码解析: Android热更新开源项目Tinker源码解析系列之一:Dex热更新 Android热更新开源项目Tinker源码解析系列之二:资源文件热更新 A ...
- 【原】Android热更新开源项目Tinker源码解析系列之一:Dex热更新
[原]Android热更新开源项目Tinker源码解析系列之一:Dex热更新 Tinker是微信的第一个开源项目,主要用于安卓应用bug的热修复和功能的迭代. Tinker github地址:http ...
随机推荐
- GO项目目录
|--bin |--pkg |--src 其中,bin存放编译后的可执行文件:pkg存放编译后的包文件:src存放项目源文件.一般,bin和pkg目录可以不创建,go命令会自动创建(如 go inst ...
- css3 transform 让 font-size 小于 12px
做页面的时候,看到一个地方要求 font-size:8px ,测试了下,浏览器果然不支持,^_^,然后就想怎么办,理所当然的掉进了 -webkit-text-size-adjust:none; 的坑, ...
- [Jenkins] 解决 Gradle 编译包含 SVG Drawable 出现异常
异常信息 java.awt.AWTError: Can't connect to X11 window server using 'localhost:10.0' as the value of th ...
- 02.ZooKeeper的Java客户端使用
1.ZooKeeper常用客户端比较 1.ZooKeeper常用客户端 zookeeper的常用客户端有3种,分别是:zookeeper原生的.Apache Curator.开源的zkclie ...
- springboot + ApplicationListener
ApplicationListener自定义侦听器类 @Component public class InstantiationTracingBeanPostProcessor implements ...
- Hadoop 启动脚本分析与实战经验
start-all.sh脚本现在已经废弃,推荐使用start-dfs.sh和start-yarn.sh分别启动HDFS和YARN. 在新一代的Hadoop里面HDFS称为了统一存储的平台,而YARN成 ...
- 【Python算法】递归与递归式
该树结构显示了从1(根节点)到n(n个叶节点)的整个倍增过程.节点下的标签表示从n减半到1的过程. 当我们处理递归的时候,这些级数代表了问题实例的数量以及对一系列递归调用来说处理的相关工作量. 当我们 ...
- d3.js 之关联数据:data操作符
数据可视化 在可视化工作中,一个基本出发点是将不同的数值映射到不同的可视化 元素的属性上,使其表现出各自不同的视觉特征. 比如:以数组中的每一个值为直径分别创建一个圆,我们得到三个圆: 在d3中,可视 ...
- appium+python自动化测试真机测试时报错“info: [debug] Error: Could not extract PIDs from ps output. PIDS: [], Procs: ["bad pid 'uiautomator'"]”
刚开始启动服务时,弹出授权提示,以为是手机app权限问题,后来debug后,发现了一个警告日志:UiAutomator did not shut down fast enough, calling i ...
- Again Array Queries---Lightoj1100(循环暴力)
题目链接:http://lightoj.com/volume_showproblem.php?problem=1100 题意是给你n个数,q个询问,每次求出 a 到 b(从0开始)最小差值: 直接暴力 ...