原生HTTP服务器
学习过Nodejs的朋友肯定对下面这段代码非常熟悉:
const http = require('http');
let server = http.createServer((req, res) => {
// ....回调函数,输出hello world
res.end('hello world!')
})
server.listen(3000)
复制代码
就这样简单几行代码,就搭建了一个简单的服务器,服务器以回调函数的形式处理HTTP请求。上面这段代码还有一种更加清晰的等价形式,代码如下:
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事件接收数据。
req是http.IncomingMessage实例(同时实现了Readable Stream接口),详情请参看文档
res是http.ServerResponse实例(同时实现了Writable Stream接口),详情请参看文档
Koa写HTTP服务器
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框架设计的理念。
Koa源代码解析
要搞懂这个原理,最好的方法就是直接查看Koa的源代码。Koa代码写的非常精简,大约1700多行,难度并非太大,值得一看。 我们以上述demo为例,进行一个分析,我把koa的执行分为两个阶段,第一个阶段:初始化阶段,主要的工作为初始化使用到的中间件(async/await形式)并在指定端口侦听,第二个阶段:请求处理阶段,请求到来,进行请求的处理。
初始化阶段
第一个阶段主要使用的两个函数就是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) => {}类型的函数。
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实现了洋葱模式。