学习过Nodejs的朋友确定对下面这段代码很是熟悉:javascript
const http = require('http'); let server = http.createServer((req, res) => { // ....回调函数,输出hello world res.end('hello world!') }) server.listen(3000) 复制代码
就这样简单几行代码,就搭建了一个简单的服务器,服务器以回调函数的形式处理HTTP请求。上面这段代码还有一种更加清晰的等价形式,代码以下:java
let server = new http.Server(); server.on("request", function(req, res){ // ....回调函数,输出hello world res.end('hello world!') }); server.listen(3000); 复制代码
首先建立了一个HttpServer的实例,对该实例进行request事件监听,server在3000端口进行监听。HttpServer继承与net.Server,它使用http_parser对链接的socket对象进行解析,当解析完成http header以后,会触发request事件,body数据继续保存在流中,直到使用data事件接收数据。node
req是http.IncomingMessage实例(同时实现了Readable Stream接口),详情请参看文档json
res是http.ServerResponse实例(同时实现了Writable Stream接口),详情请参看文档api
Koa 应用程序是一个包含一组中间件函数的对象,它是按照相似堆栈的方式组织和执行的。服务器
const Koa = require('koa'); const app = new Koa(); app.use(async ctx => { ctx.body = 'Hello World'; }); app.listen(3000); 复制代码
Koa写http服务器的形式与咱们直接经过node http模块写的方式差异很大。第一部分析可知,node的http服务器建立来自于http.createServer等方法,Koa中是如何从原生方法封装成koa形式的服务器呢?搞懂这个原理也就搞懂了Koa框架设计的理念。markdown
要搞懂这个原理,最好的方法就是直接查看Koa的源代码。Koa代码写的很是精简,大约1700多行,难度并不是太大,值得一看。 咱们以上述demo为例,进行一个分析,我把koa的执行分为两个阶段,第一个阶段:初始化阶段,主要的工做为初始化使用到的中间件(async/await形式)并在指定端口侦听,第二个阶段:请求处理阶段,请求到来,进行请求的处理。app
第一个阶段主要使用的两个函数就是app.use和app.listen。这两个函数存在application.js中。 app.use最主要的功能将中间件推入一个叫middleware的list中。框架
use(fn) { ... this.middleware.push(fn); return this; } 复制代码
listen的主要做用就是采用咱们第一部分的方式建立一个http服务器并在指定端口进行监听。request事件的监听函数为this.callback(),它返回(req, res) => {}类型的函数。koa
listen(...args) { const server = http.createServer(this.callback()); return server.listen(...args); } 复制代码
分析一下callback函数,代码以下:
/** * Return a request handler callback * for node's native http server. * * @return {Function} * @api public */ callback() { const fn = compose(this.middleware); // 将中间件函数合成一个函数fn // ... const handleRequest = (req, res) => { const ctx = this.createContext(req, res); // 使用req和res建立一个上下文环境ctx return this.handleRequest(ctx, fn); }; return handleRequest; } 复制代码
至此第一个阶段完成,经过源代码的分析,咱们能够知道它实际执行的内容跟咱们第一部分使用node http模块执行的大概一致。这里有一个疑问,compose函数是怎么实现的呢?async/await函数返回形式为Promise,怎么保证它的顺序执行呢?一开始个人猜测是将下一个middleware放在上一个middleware执行结果的then方法中,大概思路以下:
compose(middleware) { return () => { let composePromise = Promise.resolve(); middleware.forEach(task => { composePromise = composePromise.then(()=>{return task&&task()}) }) return composePromise; } } 复制代码
最终达到的效果为:f1().then(f2).then(f3).. Koa在koa-compose中用了另一种方式:
function compose (middleware) { // ... return function (context, next) { // last called middleware # let index = -1 return dispatch(0) function dispatch (i) { if (i <= index) return Promise.reject(new Error('next() called multiple times')) index = i let fn = middleware[i] if (i === middleware.length) fn = next if (!fn) return Promise.resolve() try { return Promise.resolve(fn(context, function next () { return dispatch(i + 1) })) } catch (err) { return Promise.reject(err) } } } } 复制代码
它从第一个中间件开始,遇到next,就中断本中间件的代码执行,跳转到对应的下一个中间件执行期内的代码…一直到最后一个中间件,而后逆序回退到倒数第二个中间件next下部分的代码执行,完成后继续会退…一直会退到第一个中间件next下部分的代码执行完成,中间件所有执行结束。从而实现咱们所说的洋葱圈模型。
当一个请求过来时,它会进入到request事件的回调函数当中,在Koa中被封装在handleRequest中:
handleRequest(ctx, fnMiddleware) { const res = ctx.res; res.statusCode = 404; // koa默认的错误处理函数,它处理的是错误致使的异常结束 const onerror = err => ctx.onerror(err); // respond函数里面主要是一些收尾工做,例如判断http code为空如何输出,http method是head如何输出,body返回是流或json时如何输出 const handleResponse = () => respond(ctx); // 第三方函数,用于监听 http response 的结束事件,执行回调 // 若是response有错误,会执行ctx.onerror中的逻辑,设置response类型,状态码和错误信息等 onFinished(res, onerror); return fnMiddleware(ctx).then(handleResponse).catch(onerror); } 复制代码
请求到来时,首先执行第一个阶段封装的compose函数,而后进入handleResponse中进行一些收尾工做。至此,完成整个请求处理阶段。
Koa是一个设计很是精简的Web框架,源代码自己不含任何中间件,可使咱们根据自身须要去组合一些中间件使用。它结合async/await实现了洋葱模式。