Koa2源码学习
Koa – 基于 Node.js 平台的下一代 web 开发框架
简单使用
const koa = require('koa');
const app = new koa();
app.use((context, next) => {
// do some thing
});
app.listen(8080);
以上代码构建了一个简单的服务器,你可以在浏览器输入 localhost:8080 来访问
下面我们通过创建koa服务器,且发送一次HTTP请求来了解源码
实例化koa前
在koa实例化前,先介绍四个对象
// lib/application.js
...
const context = require('./context');
const request = require('./request');
const response = require('./response');
...
// 实例化前,主要是引入koa三个对象
- Application对象:koa 应用对象,封装了端口监听,请求回调,中间件使用…
- Context对象:上下文对象,封装了Response、Request及HTTP原生response、request对象的方法和属性
- Request对象: koa Request对象,是对http.IncomingMessage的抽象
- Response对象:koa Response对象,是对http.ServerResponse的抽象
Context
koa上下文对象,大部分操作都是通过ctx(context简写)完成的
Context对象包含两部分:
- 自身对象的属性和方法
- 通过delegate方法委托的Response对象及Request对象的属性和方法
// lib/context.js
// context对象自身的方法及属性
const proto = module.exports = {
...
inspect() {
if (this === proto) return this;
return this.toJSON();
},
...
}
// 委托的Request和Response对象的方法
delegate(proto, 'response')
.method('attachment')
...
delegate(proto, 'request')
.method('acceptsLanguages')
...
Request
对http.IncomingMessage的抽象,提供了很多方法和属性,使操作更加方便
// lib/request.js
module.exports = {
...
get header() {
return this.req.headers;
},
set header(val) {
this.req.headers = val;
},
...
}
Response
对http.ServerResponse的抽象,提供了很多方法和属性,使操作更加方便
// lib/response.js
module.exports = {
...
get socket() {
return this.res.socket;
},
get header() {
const { res } = this;
return typeof res.getHeaders === 'function'
? res.getHeaders()
: res._headers || {}; // Node < 7.7
},
...
}
实例化
...
// 应用代码
const koa = require('koa');
const app = new koa();
...
实例化koa(即Application对象)时,执行constructor里面代码
// lib/application.js
...
constructor() {
super();
...
this.middleware = [];
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
...
}
...
// 主要是赋值,这里的middleware就是存储中间件的数组
值得注意的是这里仅仅是赋值,并没有任何调用HTTP服务的代码
Object.create语法参考
调用listen方法
// 应用代码
app.listen(8080);
使用listen方法时,调用Application的listen方法
// lib/application.js
...
listen(...args) {
debug('listen');
const server = http.createServer(this.callback());
return server.listen(...args);
}
...
// listen方法就是是HTTP的语法糖
这里值得注意的是
- this.callback返回值会自动添加到 "request"事件中去,每当有HTTP请求时,都会去执行
- server.listen(…args)返回一个net.Server实例,可以控制该服务器,你可以通过返回值来控制当前服务器,例如关闭服务器
node api:http_http_createserver 和 net_server_listen
callback方法
在listen方法中,主要是this.callback方法,其他都是http基本用法
...
// lib/application.js
callback() {
const fn = compose(this.middleware);
if (!this.listenerCount('error')) this.on('error', this.onerror);
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};
return handleRequest;
}
...
callback重要的两个部分:
- compose初始化中间件
- handleRequest加载中间件且处理响应
以下分别来介绍这两个部分
compose方法
在koa中,中间件非常重要,绝大部分多功能都是由中间件来实现的,中间件重要的几个特点:
- 保证中间件的调用顺序,使用next方法调用下一个中间件
- 洋葱圈模型
- 支持async/await
让我们看一下koa-compose是如何实现的
...
// koa-compose/index.js
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 {
// 最重要的代码 实现next方法和洋葱圈
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}
}
...
以上代码中,最重要的dispatch函数,通过递归dispatch,来实现中间件的连续且按顺序调用
以两个中间件为例子
app.use((context, next) => {
// do some thing1
next()
// do some thing3
});
app.use((context, next) => {
// do some thing2
});
-
调用第一个中间件
-
调用dispatch(0)
-
调用fn(context, dispatch.bind(null, i + 1))
next的实现:- 中间件需要两个传参,第一个参数为context,第二个参数为next,此时next为dispatch.bind(null, i + 1),即next此时是第二个中间件。当你在第一个中间件内部执行next方法时,其实调用的是第二个中间件,依次类推;
-
执行第一个中间件代码:do some thing1
-
调用了next方法
-
执行第二个中间件代码:do some thing2,后续无中间件,即返回
-
next返回后,再次执行第一个中间件 do some thing3
-
执行完毕后返回
这就是koa中间件的特性之一,洋葱圈的原理
值得注意的是:
- fn(context, dispatch.bind(null, i + 1)) ,这里使用了bind方法,如果这里不使用bind的方法话,第二个中间件会立即执行,无论你是否调用next方法;
- 这里的返回值都是Promise类型,为了支持async/await语法
- 在使用async函数后,可以阻塞异步操作,保证中间件执行顺序
handleRequest方法
这里调用了两个方法createContext和handleRequest
// lib/application.js
...
const handleRequest = (req, res) => {
// 这里接受的req,res参数为http.IncomingMessage和http.ServerResponse的实例
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};
...
createContext
createContext方法主要是创建context对象及赋值
// lib/application.js
...
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;
}
...
这里值得注意的:
- context和this.context不相等,即在使用中间件的时候,context和context.app.context是不相等的
- 因为this.callback是在每次"request"请求时调用的,每个request请求都会调用createContext方法,即每个请求的context都是不一样的,可以放心操作
handleRequest
该方法主要是调用中间件及处理响应
// lib/application.js
...
handleRequest(ctx, fnMiddleware) {
// fnMiddleware为compose处理后的返回函数,当调用该函数时,即调用整个中间件
const res = ctx.res;
res.statusCode = 404;
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx); // 组装返回值的方法
onFinished(res, onerror);
return fnMiddleware(ctx).then(handleResponse).catch(onerror); //此步骤执行中间件且处理响应,在中间件全部未抛出错误的下,对响应值进行处理,否则捕获错误
}
...
use方法
// 应用代码
app.use((context, next) => {
// do some thing1
next()
// do some thing3
});
使用use方法时,调用Application的use方法
// lib/application.js
...
use(fn) {
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
if (isGeneratorFunction(fn)) {
deprecate('Support for generators will be removed in v3. ' +
'See the documentation for examples of how to convert old middleware ' +
'https://github.com/koajs/koa/blob/master/docs/migration.md');
fn = convert(fn);
}
debug('use %s', fn._name || fn.name || '-');
this.middleware.push(fn); // 将中间件存入数组中
return this;
}
...
创建一个简单服务器及响应一次请求
下面按照"创建一个简单服务器及响应一次请求"来阐述代码执行顺序
-
实例化Application,声明对象属性、方法及赋值
-
执行listen函数
-
执行callback函数
- 执行koa-compose的compose函数,返回一个匿名函数
- 返回handleRequet函数,但并不执行,且此函数为"request"事件的回调函数
-
服务器收到请求,触发"request"事件
-
执行handleRequest函数
- 执行createContext函数,生成上下文
- 执行handleRequest函数(与上文的handleRequest不是一个函数,具体可以看代码)
- 生成处理响应函数和错误捕获函数
- 执行中间件
- 中间件执行文完毕,成功则进入响应函数,错误则进入错误捕获函数
- 响应函数根据内容和状态码返回不同的值
- 错误捕获函数捕获错误,抛出