深入解读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 相关文章推荐
Android中资源文件(非代码部分)的使用概览
Dec 18 Javascript
javascript中RegExp保留小数点后几位数的方法分享
Aug 13 Javascript
高效的获取当前元素是父元素的第几个子元素
Oct 15 Javascript
Lua表达式和控制结构学习笔记
Dec 15 Javascript
JavaScript淡入淡出渐变简单实例
Aug 06 Javascript
JavaScript encodeURI 和encodeURIComponent
Dec 04 Javascript
json格式数据的添加,删除及排序方法
Jan 21 Javascript
JS实现的加减乘除四则运算计算器示例
Aug 09 Javascript
web页面和微信小程序页面实现瀑布流效果
Sep 26 Javascript
js的对象与函数详解
Jan 21 Javascript
基于Vue 实现一个中规中矩loading组件
Apr 03 Javascript
关于angular引入ng-zorro的问题浅析
Sep 09 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
php+resumablejs实现的分块上传 断点续传功能示例
2017/04/18 PHP
php 生成加密公钥加密私钥实例详解
2017/06/16 PHP
实例讲解PHP中使用命名空间
2019/01/27 PHP
javascript 设为首页与加入收藏兼容多浏览器代码
2011/01/11 Javascript
浅析onsubmit校验表单时利用ajax的return false无效问题
2013/07/10 Javascript
网站内容禁止复制和粘贴、另存为的js代码
2014/02/26 Javascript
Jquery 返回json数据在IE浏览器中提示下载的问题
2014/05/18 Javascript
用html+css+js实现的一个简单的图片切换特效
2014/05/28 Javascript
使用insertAfter()方法在现有元素后添加一个新元素
2014/05/28 Javascript
js获得当前系统日期时间的方法
2015/05/06 Javascript
javascript中判断json的方法总结
2015/08/27 Javascript
jQuery鼠标事件汇总
2015/08/30 Javascript
原生js实现电商侧边导航效果
2017/01/19 Javascript
javascript 实现文本使用省略号替代(超出固定高度的情况)
2017/02/21 Javascript
JS实现合并json对象的方法
2017/10/10 Javascript
jquery在启动页面时,自动加载数据的实例
2018/01/22 jQuery
JS代码实现电脑配置检测功能
2018/03/21 Javascript
jQuery实现获取动态添加的标签对象示例
2018/06/28 jQuery
koa大型web项目中使用路由装饰器的方法示例
2019/04/02 Javascript
vuex实现像调用模板方法一样调用Mutations方法
2019/11/06 Javascript
详解小程序横屏方案对比
2020/06/28 Javascript
使用python在校内发人人网状态(人人网看状态)
2014/02/19 Python
python压缩文件夹内所有文件为zip文件的方法
2015/06/20 Python
使用XML库的方式,实现RPC通信的方法(推荐)
2017/06/14 Python
python虚拟环境virtualenv的安装与使用
2017/09/21 Python
详解Django中类视图使用装饰器的方式
2018/08/12 Python
利用Python检测URL状态
2019/07/31 Python
PyTorch里面的torch.nn.Parameter()详解
2020/01/03 Python
移动端Web页面的CSS3 flex布局快速上手指南
2016/05/31 HTML / CSS
英国网上超市:Ocado
2020/03/05 全球购物
几道Java和数据库的面试题
2013/05/30 面试题
口腔医学技术应届生求职信
2013/11/09 职场文书
cf战队宣传语
2015/07/13 职场文书
爱护公物主题班会
2015/08/17 职场文书
豆瓣2021评分最高动画剧集-豆瓣评分最高的动画剧集2021
2022/03/18 日漫
台式电脑蓝牙适配器怎么安装?台式电脑蓝牙适配器安装教程
2022/04/08 数码科技