本文共 6989 字,大约阅读时间需要 23 分钟。
Node.js也是写了两三年的时间了,刚开始学习Node
的时候,hello world
就是创建一个HttpServer
,后来在工作中也是经历过Express
、Koa1.x
、Koa2.x
以及最近还在研究的结合着TypeScript
的routing-controllers
(驱动依然是Express
与Koa
)。
Koa
版本,也是对它的洋葱模型比较感兴趣,所以最近抽出时间来阅读其源码,正好近期可能会对一个Express
项目进行重构,将其重构为koa2.x
版本的,所以,阅读其源码对于重构也是一种有效的帮助。 首先需要确定,Koa是什么。
任何一个框架的出现都是为了解决问题,而Koa则是为了更方便的构建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'))
一个最简单的示例,脚本运行后访问即可看到一个
Hello World
的字符串。
> 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) // 返回数据}
但是这样的写法还会带来另一个问题,如果是一个很大的项目,存在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'))
这样,就实现了一个代码比较整洁的HttpServer
,但功能上依旧是很简陋的。
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 abefore bafter abefore 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版本使用将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
内部做的处理。
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
相关的源码翻看一波,看得挺激动的,想要将它们记录下来。
转载地址:http://tyqya.baihongyu.com/