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

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 :8888'))

一个最简单的示例,脚本运行后访问:8888即可看到一个Hello World的字符串。
但是这仅仅是一个简单的示例,因为我们不管访问什么地址(甚至修改请求的Method),都总是会获取到这个字符串:

> curl :8888 > curl :8888/sub > curl -X POST :8888

所以我们可能会在回调中添加逻辑,根据路径、Method来返回给用户对应的数据:

const serverHandler = (request, response) => { // default let responseData = '404' if (request.url === 'https://www.jb51.net/') { 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('https://www.jb51.net/', function (request, response) { response.end('Hello World') }) app.post('https://www.jb51.net/', 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 :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('https://www.jb51.net/', generatorId, function(request, response) { response.end(`Hello World ${this.id}`) })

这样在访问接口时,就可以看到Hello World 123的字样了。

这个就可以简单的认为是在Express中实现的 中间件。

中间件是Express、Koa的核心所在,一切依赖都通过中间件来进行加载。

更灵活的中间件方案-洋葱模型

上述方案的确可以让人很方便的使用一些中间件,在流程控制中调用next()来进入下一个环节,整个流程变得很清晰。

但是依然存在一些局限性。

例如如果我们需要进行一些接口的耗时统计,在Express有这么几种可以实现的方案:

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:http://www.heiqu.com/06ab286a0fe90f6892e979026d11938e.html