深入解读Node.js中的koa源码


Posted in Javascript onJune 17, 2019

前言

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)

由于在功能、使用上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中间件执行的流程:

  1. 获取当前的时间戳requestTime
  2. 调用next()执行后续的中间件,并监听其回调
  3. 第二个中间件里边可能会调用第三个、第四个、第五个,但这都不是log所关心的,log只关心第二个中间件何时resolve,而第二个中间件的resolve则依赖他后边的中间件的resolve。
  4. 等到第二个中间件resolve,这就意味着后续没有其他的中间件在执行了(全都resolve了),此时log才会继续后续代码的执行

所以就像洋葱一样一层一层的包裹,最外层是最大的,是最先执行的,也是最后执行的。(在一个完整的请求中,next之前最先执行,next之后最后执行)

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
JavaScript中的对象化编程
Jan 16 Javascript
jQuery登陆判断简单实现代码
Apr 21 Javascript
jquery取消选择select下拉框示例代码
Feb 22 Javascript
node.js中的fs.fsync方法使用说明
Dec 15 Javascript
jquery实现拖拽调整Div大小
Jan 30 Javascript
开启Javascript中apply、call、bind的用法之旅模式
Oct 28 Javascript
JavaScript仿聊天室聊天记录
Dec 27 Javascript
svg动画之动态描边效果
Feb 22 Javascript
jQuery插件HighCharts绘制简单2D柱状图效果示例【附demo源码】
Mar 21 jQuery
原生js实现密码输入框值的显示隐藏
Jul 17 Javascript
Vue2.0权限树组件实现代码
Aug 29 Javascript
使用layui实现树形结构的方法
Sep 20 Javascript
学习RxJS之JavaScript框架Cycle.js
Jun 17 #Javascript
javascript系统时间设置操作示例
Jun 17 #Javascript
深入学习TypeScript 、React、 Redux和Ant-Design的最佳实践
Jun 17 #Javascript
Vue程序调试的方法
Jun 17 #Javascript
Vue拖拽组件列表实现动态页面配置功能
Jun 17 #Javascript
javascript实现日历效果
Jun 17 #Javascript
如何使用Node.js爬取任意网页资源并输出PDF文件到本地
Jun 17 #Javascript
You might like
JavaScript中的new的使用方法与注意事项
2007/05/16 Javascript
常用的javascript function代码
2008/05/23 Javascript
用JS剩余字数计算的代码
2008/07/03 Javascript
Mootools 1.2教程 同时进行多个形变动画
2009/09/15 Javascript
这段js代码得节约你多少时间
2011/12/20 Javascript
JS无法捕获滚动条上的mouse up事件的原因猜想
2012/03/21 Javascript
使用jquery实现图文切换效果另加特效
2013/01/20 Javascript
jqueryMobile使用示例分享
2016/01/12 Javascript
Javascript之String对象详解
2016/06/08 Javascript
JS实现复制内容到剪贴板功能兼容所有浏览器(推荐)
2016/06/17 Javascript
JSON 对象未定义错误的解决方法
2016/09/29 Javascript
EasyUI学习之Combobox级联下拉列表(2)
2016/12/29 Javascript
手机端转换rem适应
2017/04/01 Javascript
从Vuex中取出数组赋值给新的数组,新数组push时报错的解决方法
2018/09/18 Javascript
详解Angular模板引用变量及其作用域
2018/11/23 Javascript
详解vue在项目中使用百度地图
2019/03/26 Javascript
20个必会的JavaScript面试题(小结)
2019/07/02 Javascript
[43:41]VP vs RNG 2019国际邀请赛淘汰赛 败者组 BO3 第二场 8.21.mp4
2020/07/19 DOTA
python解决字典中的值是列表问题的方法
2013/03/04 Python
Python2和3字符编码的区别知识点整理
2019/08/08 Python
手把手教你安装Windows版本的Tensorflow
2020/03/26 Python
使用python批量转换文件编码为UTF-8的实现
2020/04/03 Python
利用python 下载bilibili视频
2020/11/13 Python
Nordgreen台湾官网:极简北欧设计手表
2019/08/21 全球购物
美国名牌香水折扣网站:Hottperfume
2021/02/10 全球购物
怎样声明一个匿名的内部类
2016/06/01 面试题
毕业生找工作推荐信
2013/11/21 职场文书
酒店办公室文员岗位职责
2013/12/18 职场文书
先进集体事迹材料
2014/02/17 职场文书
勤俭节约倡议书
2014/04/14 职场文书
2014年秋季开学典礼主持词
2014/08/02 职场文书
个人买房协议书范本
2014/10/06 职场文书
2015年技术工作总结范文
2015/04/20 职场文书
2015年库房工作总结
2015/04/30 职场文书
Android Rxjava3 使用场景详解
2022/04/07 Java/Android
Spring Cloud OAuth2实现自定义token返回格式
2022/06/25 Java/Android