Koa 源码分析
koa
的源码文件很少,lib 文件夹下只有4个文件 application、context、request、response
。先看一下大概的调用关系:
在 package.json 文件中找到 koa 的入口文件 lib/application.js
。下面我们进入 lib/application.js
文件中看看一下源码的实现。
application
首先我们把代码精简一下,去掉无关的注释,以及一些验证处理,先找出实现的主流程逻辑:
导出的是 application 模块,这个模块继承了 events 类,然后是constrouctor里面挂载了很多的属性和方法。有三个重要对象是context,request,response,还有一个用来存储中间件函数的数组middleware。把无关代码先去掉,看最主要的:
const context = require('./context');
const request = require('./request');
const response = require('./response');
module.exports = class Application extends Emitter {
constructor(options) {
super();
options = options || {};
this.middleware = [];
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
}
}
context,request ,response 就是剩余的三个核心文件:上下文,请求和响应,暂且不看,继续往下。
到真正发起请求是调用 listen
方法,这方法封装了原始的 http 创建服务的请求:
const http = require('http');
listen(...args) {
debug('listen');
const server = http.createServer(this.callback());
return server.listen(...args);
}
listen 方法 http.createServer
传入this.callback(),this.callback
里面调到 this.createContext
来创建上下文,可以看到很多的原生的 Node 的 request
和 response
被挂到 context
对象上,这样可以直接使用ctx.res
,ctx.req
的方式访问对象:
callback() {
const fn = compose(this.middleware);
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};
return handleRequest;
}
createContext(req, res) {
const context = Object.create(this.context);
const request = context.request = Object.create(this.request);
const response = context.response = Object.create(this.response);
context.app = request.app = response.app = this;
context.req = request.req = response.req = req;
context.res = request.res = response.res = res;
request.ctx = response.ctx = context;
request.response = response;
response.request = request;
context.originalUrl = request.originalUrl = req.url;
context.state = {};
return context;
}
callback 函数最后返回了一个 handleRequest
函数,这个函数接收两个参数,原生 Node 中 http 模块的request
和 response
,最后返回了 this.handleRequest
函数,这个函数接收上下文 ctx
,和一个compose
之后的 fn
,这个 compose
方法就是处理中间件的机制,这个中间件处理我们最后看。先把流程理清。
再看这个this.handleRequest
函数的实现,同样去掉一些错误的处理,代码实现就三行:
handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
const handleResponse = () => respond(ctx);
return fnMiddleware(ctx).then(handleResponse);
}
this.handleRequest
,拿到ctx.res
,然后处理响应的结果,这个处理调了respond
来处理响应,fnMiddleware(ctx)
这个表示所有的中间件全部处理完成之后,统一处理响应response
结果。再看看respond
是如何实现的,同样去掉一些验证的处理,也就剩几行代码:
function respond(ctx) {
if (Buffer.isBuffer(body)) return res.end(body);
if ('string' == typeof body) return res.end(body);
if (body instanceof Stream) return body.pipe(res);
res.end(body);
}
可以看到这个响应返回的类型有三种:Buffer类型,字符串类型和stream类型
。
再看use
方法,同样去掉干扰代码,也就两行:
use(fn) {
this.middleware.push(fn);
return this;
}
use
中的参数就表示中间件函数,use
方法就是收集所有的中间件函数放入到数组中,然后返回this,继续向下执行。middleware
数组被传给compose
库来处理中间件机制。
到这里主流程就结束了。
我们总结一下:创建服务createServert ->listen监听进来的请求 -> 处理中间件compose->响应结果。
context、request、response
context
文件里面主要做一些request response 属性委托
,让context
本身具有这些属性,可以直接通过如ctx.url,ctx.body
的方式设置获取数据,而不用通过ctx.request.url,ctx.response.body
的方式。
request, response
文件里面 主要是get set
的属性操作。
中间件机制compose
这个是引用的 koa-compose
模块,这个模块用来处理koa
的中间件数组。下面在node_modules
里面找到koa-compose
模块,找到入口文件index.js
,来看看里面是如何实现的。按照同样的方法,先去掉干扰代码,尽量干净,我们只看核心实现,可以看到实现非常的简洁:
module.exports = compose
function compose (middleware) {
return function (context, next) {
let index = -1
return dispatch(0)
function dispatch (i) {
index = i
let fn = middleware[i]
if (i === middleware.length) fn = next
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
}
}
}
compose
函数传入中间件数组,就是application
上的middleware
,我们先写几个中间件函数,然后对应到代码里面的实现,这样更容易看些:
const app= new Koa()
const mid1 = async (ctx, next) => {
ctx.body = 'hello';
next();
}
const mid2 = async (ctx, next) => {
ctx.body = ctx.body + ' world';
next();
}
app.use(mid1)
app.use(mid2)
这时middleware = [mid1, mid2]
现在调用listen方法,将参数往下传递,直到调用this.callback
里面的fn = compose(middleware)
,可以调试代码,看fn到底是什么,执行fn.toString()
,看到如下代码:
function (context, next) {
let index = -1
return dispatch(0)
function dispatch (i) {
index = i
let fn = middleware[i]
if (i === middleware.length) fn = next
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
}
}
compose(middleware)
返回一个函数,最主要的实现是 dispatch
:
function dispatch (i) {
index = i
let fn = middleware[i]
if (i === middleware.length) fn = next
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
}
this.callback
返回this.handleRequest(ctx, fn)
; this.handleRequest
调用fn(ctx)
,执行fn(ctx)
首先return dispatch(0)
,return Promise.resolve(mid1(context, dispatch.bind(null, i + 1)))
,此时的 mid1
的 next
就是 dispatch.bind(null, i + 1)
,调用next
就是继续调用dispatch.bind(null, i + 1)
,索引加1
继续往下一个中间件执行,程序运行到netx()
的时候会暂停当前的程序运行,进入到下一个中间件,直到把middleware
数组中的函数next()
之前的代码都执行一遍之后,i === middleware.length
,fn = next
,起初设置index = -1是为了将全部中间件函数里next()
之前的部分执行完成之后,在从最后一项next()
向上一个个穿过中间件函数执行各个中间件函数next()
之后的代码,这个流程就是koa
的洋葱模型:
总结来说Koa的重要实现有两点:
1. context的保存和传递; 2.中间件的管理和next的处理;