koa源码阅读[0]
koa源码阅读[0]
Node.js也是写了两三年的时间了,刚开始学习Node的时候,hello world就是创建一个HttpServer,后来在工作中也是经历过Express、Koa1.x、Koa2.x以及最近还在研究的结合着TypeScript的routing-controllers(驱动依然是Express与Koa)。
用的比较多的还是Koa版本,也是对它的洋葱模型比较感兴趣,所以最近抽出时间来阅读其源码,正好近期可能会对一个Express项目进行重构,将其重构为koa2.x版本的,所以,阅读其源码对于重构也是一种有效的帮助。
Koa是怎么来的
首先需要确定,Koa是什么。
任何一个框架的出现都是为了解决问题,而Koa则是为了更方便的构建http服务而出现的。
可以简单的理解为一个HTTP服务的中间件框架。
使用http模块创建http服务
相信大家在学习Node时,应该都写过类似这样的代码:
const http = require('http')
const serverHandler = (request, response) => {
response.end('Hello World') // 返回数据
}
http
.createServer(serverHandler)
.listen(8888, _ => console.log('Server run as http://127.0.0.1:8888'))
一个最简单的示例,脚本运行后访问http://127.0.0.1:8888即可看到一个Hello World的字符串。
但是这仅仅是一个简单的示例,因为我们不管访问什么地址(甚至修改请求的Method),都总是会获取到这个字符串:
> curl http://127.0.0.1:8888
> curl http://127.0.0.1:8888/sub
> curl -X POST http://127.0.0.1:8888
所以我们可能会在回调中添加逻辑,根据路径、Method来返回给用户对应的数据:
const serverHandler = (request, response) => {
// default
let responseData = '404'
if (request.url === '/') {
if (request.method === 'GET') {
responseData = 'Hello World'
} else if (request.method === 'POST') {
responseData = 'Hello World With POST'
}
} else if (request.url === '/sub') {
responseData = 'sub page'
}
response.end(responseData) // 返回数据
}
类似Express的实现
但是这样的写法还会带来另一个问题,如果是一个很大的项目,存在N多的接口。
如果都写在这一个handler里边去,未免太过难以维护。
示例只是简单的针对一个变量进行赋值,但是真实的项目不会有这么简单的逻辑存在的。
所以,我们针对handler进行一次抽象,让我们能够方便的管理路径:
class App {
constructor() {
this.handlers = {}
this.get = this.route.bind(this, 'GET')
this.post = this.route.bind(this, 'POST')
}
route(method, path, handler) {
let pathInfo = (this.handlers[path] = this.handlers[path] || {})
// register handler
pathInfo[method] = handler
}
callback() {
return (request, response) => {
let { url: path, method } = request
this.handlers[path] && this.handlers[path][method]
? this.handlers[path][method](request, response)
: response.end('404')
}
}
}
然后通过实例化一个Router对象进行注册对应的路径,最后启动服务:
const app = new App()
app.get('/', function (request, response) {
response.end('Hello World')
})
app.post('/', function (request, response) {
response.end('Hello World With POST')
})
app.get('/sub', function (request, response) {
response.end('sub page')
})
http
.createServer(app.callback())
.listen(8888, _ => console.log('Server run as http://127.0.0.1:8888'))
Express中的中间件
这样,就实现了一个代码比较整洁的HttpServer,但功能上依旧是很简陋的。
如果我们现在有一个需求,要在部分请求的前边添加一些参数的生成,比如一个请求的唯一ID。
将代码重复编写在我们的handler中肯定是不可取的。
所以我们要针对route的处理进行优化,使其支持传入多个handler:
route(method, path, ...handler) {
let pathInfo = (this.handlers[path] = this.handlers[path] || {})
// register handler
pathInfo[method] = handler
}
callback() {
return (request, response) => {
let { url: path, method } = request
let handlers = this.handlers[path] && this.handlers[path][method]
if (handlers) {
let context = {}
function next(handlers, index = 0) {
handlers[index] &&
handlers[index].call(context, request, response, () =>
next(handlers, index + 1)
)
}
next(handlers)
} else {
response.end('404')
}
}
}
然后针对上边的路径监听添加其他的handler:
function generatorId(request, response, next) {
this.id = 123
next()
}
app.get('/', generatorId, function(request, response) {
response.end(`Hello World ${this.id}`)
})
这样在访问接口时,就可以看到Hello World 123的字样了。
这个就可以简单的认为是在Express中实现的 中间件。
中间件是Express、Koa的核心所在,一切依赖都通过中间件来进行加载。
更灵活的中间件方案-洋葱模型
上述方案的确可以让人很方便的使用一些中间件,在流程控制中调用next()来进入下一个环节,整个流程变得很清晰。
但是依然存在一些局限性。
例如如果我们需要进行一些接口的耗时统计,在Express有这么几种可以实现的方案:
function beforeRequest(request, response, next) {
this.requestTime = new Date().valueOf()
next()
}
// 方案1. 修改原handler处理逻辑,进行耗时的统计,然后end发送数据
app.get('/a', beforeRequest, function(request, response) {
// 请求耗时的统计
console.log(
`${request.url} duration: ${new Date().valueOf() - this.requestTime}`
)
response.end('XXX')
})
// 方案2. 将输出数据的逻辑挪到一个后置的中间件中
function afterRequest(request, response, next) {
// 请求耗时的统计
console.log(
`${request.url} duration: ${new Date().valueOf() - this.requestTime}`
)
response.end(this.body)
}
app.get(
'/b',
beforeRequest,
function(request, response, next) {
this.body = 'XXX'
next() // 记得调用,不然中间件在这里就终止了
},
afterRequest
)
无论是哪一种方案,对于原有代码都是一种破坏性的修改,这是不可取的。
因为Express采用了response.end()的方式来向接口请求方返回数据,调用后即会终止后续代码的执行。
而且因为当时没有一个很好的方案去等待某个中间件中的异步函数的执行。
function a(_, _, next) {
console.log('before a')
let results = next()
console.log('after a')
}
function b(_, _, next) {
console.log('before b')
setTimeout(_ => {
this.body = 123456
next()
}, 1000)
}
function c(_, response) {
console.log('before c')
response.end(this.body)
}
app.get('/', a, b, c)
就像上述的示例,实际上log的输出顺序为:
before a
before b
after a
before c
这显然不符合我们的预期,所以在Express中获取next()的返回值是没有意义的。
所以就有了Koa带来的洋葱模型,在Koa1.x出现的时间,正好赶上了Node支持了新的语法,Generator函数及Promise的定义。
所以才有了co这样令人惊叹的库,而当我们的中间件使用了Promise以后,前一个中间件就可以很轻易的在后续代码执行完毕后再处理自己的事情。
但是,Generator本身的作用并不是用来帮助我们更轻松的使用Promise来做异步流程的控制。
所以,随着Node7.6版本的发出,支持了async、await语法,社区也推出了Koa2.x,使用async语法替换之前的co+Generator。
Koa也将co从依赖中移除(2.x版本使用koa-convert将Generator函数转换为promise,在3.x版本中将直接不支持Generator)
ref: remove generator supports
由于在功能、使用上Koa的两个版本之间并没有什么区别,最多就是一些语法的调整,所以会直接跳过一些Koa1.x相关的东西,直奔主题。
在Koa中,可以使用如下的方式来定义中间件并使用:
async function log(ctx, next) {
let requestTime = new Date().valueOf()
await next()
console.log(`${ctx.url} duration: ${new Date().valueOf() - requestTime}`)
}
router.get('/', log, ctx => {
// do something...
})
因为一些语法糖的存在,遮盖了代码实际运行的过程,所以,我们使用Promise来还原一下上述代码:
function log() {
return new Promise((resolve, reject) => {
let requestTime = new Date().valueOf()
next().then(_ => {
console.log(`${ctx.url} duration: ${new Date().valueOf() - requestTime}`)
}).then(resolve)
})
}
大致代码是这样的,也就是说,调用next会给我们返回一个Promise对象,而Promise何时会resolve就是Koa内部做的处理。
可以简单的实现一下(关于上边实现的App类,仅仅需要修改callback即可):
callback() {
return (request, response) => {
let { url: path, method } = request
let handlers = this.handlers[path] && this.handlers[path][method]
if (handlers) {
let context = { url: request.url }
function next(handlers, index = 0) {
return new Promise((resolve, reject) => {
if (!handlers[index]) return resolve()
handlers[index](context, () => next(handlers, index + 1)).then(
resolve,
reject
)
})
}
next(handlers).then(_ => {
// 结束请求
response.end(context.body || '404')
})
} else {
response.end('404')
}
}
}
每次调用中间件时就监听then,并将当前Promise的resolve与reject处理传入Promise的回调中。
也就是说,只有当第二个中间件的resolve被调用时,第一个中间件的then回调才会执行。
这样就实现了一个洋葱模型。
就像我们的log中间件执行的流程:
- 获取当前的时间戳
requestTime - 调用
next()执行后续的中间件,并监听其回调 - 第二个中间件里边可能会调用第三个、第四个、第五个,但这都不是
log所关心的,log只关心第二个中间件何时resolve,而第二个中间件的resolve则依赖他后边的中间件的resolve。 - 等到第二个中间件
resolve,这就意味着后续没有其他的中间件在执行了(全都resolve了),此时log才会继续后续代码的执行
所以就像洋葱一样一层一层的包裹,最外层是最大的,是最先执行的,也是最后执行的。(在一个完整的请求中,next之前最先执行,next之后最后执行)。
小记
最近抽时间将Koa相关的源码翻看一波,看得挺激动的,想要将它们记录下来。
应该会拆分为几段来,不一篇全写了,上次写了个装饰器的,太长,看得自己都困了。
先占几个坑:
- 核心模块 koa与koa-compose
- 热门中间件 koa-router与koa-views
- 杂七杂八的轮子 koa-bodyparser/multer/better-body/static
koa源码阅读[0]的更多相关文章
- koa源码阅读[3]-koa-send与它的衍生(static)
koa源码阅读的第四篇,涉及到向接口请求方提供文件数据. 第一篇:koa源码阅读-0第二篇:koa源码阅读-1-koa与koa-compose第三篇:koa源码阅读-2-koa-router 处理静态 ...
- koa源码阅读[2]-koa-router
koa源码阅读[2]-koa-router 第三篇,有关koa生态中比较重要的一个中间件:koa-router 第一篇:koa源码阅读-0第二篇:koa源码阅读-1-koa与koa-compose k ...
- koa源码阅读[1]-koa与koa-compose
接上次挖的坑,对koa2.x相关的源码进行分析 第一篇.不得不说,koa是一个很轻量.很优雅的http框架,尤其是在2.x以后移除了co的引入,使其代码变得更为清晰. express和koa同为一批人 ...
- Yii2.0源码阅读-一次请求的完整过程
Yii2.0框架源码阅读,从请求发起,到结束的运行步骤 其实最初阅读是从yii\web\UrlManager这个类开始看起,不断的寻找这个类中方法的调用者,最终回到了yii\web\Applicati ...
- redis 5.0.7 源码阅读——整数集合intset
redis中整数集合intset相关的文件为:intset.h与intset.c intset的所有操作与操作一个排序整形数组 int a[N]类似,只是根据类型做了内存上的优化. 一.数据结构 ty ...
- redis 5.0.7 源码阅读——跳跃表skiplist
redis中并没有专门给跳跃表两个文件.在5.0.7的版本中,结构体的声明与定义.接口的声明在server.h中,接口的定义在t_zset.c中,所有开头为zsl的函数. 一.数据结构 单个节点: t ...
- redis 5.0.7 源码阅读——字典dict
redis中字典相关的文件为:dict.h与dict.c 与其说是一个字典,道不如说是一个哈希表. 一.数据结构 dictEntry typedef struct dictEntry { void * ...
- redis 5.0.7 源码阅读——双向链表
redis中双向链表相关的文件为:adlist.h与adlist.c 一.数据结构 redis里定义的双向链表,与普通双向链表大致相同 单个节点: typedef struct listNode { ...
- redis 5.0.7 源码阅读——动态字符串sds
redis中动态字符串sds相关的文件为:sds.h与sds.c 一.数据结构 redis中定义了自己的数据类型"sds",用于描述 char*,与一些数据结构 typedef c ...
随机推荐
- SSH框架配置
--------------------------------applicationContext.xml-------------------------------- <?xml vers ...
- TP中循环遍历
循环遍历(重点) 在ThinkPHP中系统提供了2个标签来实现数组在模版中的遍历: volist标签.foreach标签. Volist语法格式: Foreach语法格式: 从上述的语法格式发现vol ...
- 利用书签功能对TDBGrid控件中多个记录的处理
DELPHI 的TDBGrid 控 件 主 要 用 来 处 理 数 据 表, 它 的 属 性 中 有 一 个dgMultiSelect, 若 此 属 性 设 定 为TRUE, 则 可 以 选 中 多 ...
- 内存测试——Android Studio中对应进程的Heap
通过Android Studio的Heap查看该程序的目前占用内存大小,多次进出界面,观察内存内存大小的变化.用Heap监测应用进程使用内存情况的步骤如下: 1. 启动Android Studio—& ...
- web端调百度地图页面
在点击进入地图的入口(下面数据是vue渲染的数据) <a class="navigation" v-if="merchant.longitude && ...
- BZOJ 2299 向量(裴蜀定理)
题意:给你一对数a,b,你可以任意使用(a,b), (a,-b), (-a,b), (-a,-b), (b,a), (b,-a), (-b,a), (-b,-a)这些向量,问你能不能拼出另一个向量(x ...
- Day21-获取用户请求相关信息及请求头
1. request里面还包含请求头等信息,可以打印看一下. views.py中的程序 from django.shortcuts import render,HttpResponse from dj ...
- 【刷题】BZOJ 2152 聪聪可可
Description 聪聪和可可是兄弟俩,他们俩经常为了一些琐事打起来,例如家中只剩下最后一根冰棍而两人都想吃.两个人都想玩儿电脑(可是他们家只有一台电脑)--遇到这种问题,一般情况下石头剪刀布就好 ...
- Oracle 高性能SQL引擎剖析----执行计划
执行计划是指示Oracle如何获取和过滤数据.产生最终结果集,是影响SQL语句执行性能的关键因素.我们在深入了解执行计划之前,首先需要知道执行计划是在什么时候产生的,以及如何让SQL引擎为语句生成执行 ...
- 【移动支付】.NET支付宝App支付接入
一.前言 最近也是为了新产品忙得起飞,博客都更新的慢了.新产品为了方便用户支付,需要支付宝扫码接入.这活落到了我的身上.产品是Windows系统下的桌面软件,通过软件生成二维码支付.界面 ...