fe-note
fe-note copied to clipboard
Koa2源码学习
第一篇: 图解Hello World学习源码
初衷
个人觉得学习源码需要带着目的去看, 才能达到效果, 但是公司又没有上Node, 没有实践怎么办呢?最近发现通过调试Koa2源码也是个不错的法子。
准备工作
- 安装node
- 安装vscode
- 学习如何在vscode下调试
关于Node下调试推荐阅读下:《Node.js 调试指南》。
我们这里只需要学习如何在vscode下调试即可。 具体就不详情说了, 见链接, 有问题我们可以讨论。
从Hello World开始学习
// 安装koa2, nodemon等后, 来个入门的hello world
const Koa = require('koa')
const app = new Koa()
const port = 3000
app.use(async (ctx, next) => {
await next()
ctx.response.status = 200
ctx.response.body = 'hello world'
})
app.listen(port, () => {
console.log(`server is running on the port: ${port}`)
})
是的没错,通过上述这个入门代码也能学习到Koa2的源码知识。
首先观察上面用到了的一些API。
new Koa()
app.use()
app.listen()
我们现在开始进入node_modules目录下找到Koa。
通过package.json得知Koa的入口文件为
"main": "lib/application.js"
// lib目录下模块
- application.js 实例
- context.js 上下文对象
- request.js 请求对象
- response.js 响应对象
现在我们当然从入口文件application.js开始。我们的实例代码第一行是new koa(); 我们肯定是有一个类,这里是开始的突破点。
// 构造函数继承node的EventEmitter类
// http://nodejs.cn/api/events.html
module.exports = class Application extends Emitter {
...
}
然后我们去打三个断点, 分别如下:
之所以在这三个地方打断点是对应前面提到的执行了Koa2的三个api.通过这三个断点我们一步步去了解Koa2内部是怎么执行的。
最开始肯定是执行constructor();部分注释见上述截图。
this.middleware = []; 这个是用来存放通过app.use()注册的中间件的。
重点接下来看app.use()。
很明显fn指的就是:
async (ctx, next) => {
await next()
ctx.response.status = 200
ctx.response.body = 'hello world'
}
在use(fn)方法中主要做了以下事情:
1. 错误校验, fn必须是函数, 否则给出错误提示
2. fn不推荐使用生成器函数, v2版本Koa2会进行转化, 但是v3就会不支持生成器函数了, 这里主要是对koa1的向下兼容。
3. 存储注册的中间件
3. return this. 支持链式调用
这个时候你可以看懂this大概有这些属性:
Application {
_events:Object {}
_eventsCount:0
_maxListeners:undefined
context:Object {}
env:"development"
middleware:Array(1) []
proxy:false
request:Object {}
response:Object {}
subdomainOffset:2
Symbol(util.inspect.custom):inspect() { … }
__proto__:EventEmitter
}
然后进入listen(), 这里有一段this.callback(), 我们需要去这个方法下打断点看执行了什么。
// 其实就是http.createServer(app.callback()).listen(...)的语法糖
listen(...args) {
debug('listen');
const server = http.createServer(this.callback());
return server.listen(...args);
}
// callback()做了以下几件事:
1. 通过compose合并中间件
2. 为应用注册error事件的监听器
3. 返回一个请求处理函数handleRequest
接下来我们看看this.createContext()和this.handleRequest(),分别打断点看代码。
note: 提一个小问题, node应该经常会发生端口占用问题。
每次请求都会创建一个上下文对象。
handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
res.statusCode = 404;
// 错误处理
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
// 通过第三方库on-finished监听http response,当请求结束时执行回调,这里传入的回调是context.onerror(err),即当错误发生时才执行。
onFinished(res, onerror);
// 即将所有中间件执行(传入请求上下文对象ctx),之后执行响应处理函数(respond(ctx)),当抛出异常时同样使用onerror(err)处理。
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
对respond打断点
/**
* Response helper.
* 在所有中间件执行完之后执行
*/
function respond(ctx) {
// allow bypassing koa
// 通过设置ctx.respond = false来跳过这个函数,但不推荐这样子
if (false === ctx.respond) return;
const res = ctx.res;
// 上下文对象不可写时也会退出该函数
if (!ctx.writable) return;
let body = ctx.body;
const code = ctx.status;
// ignore body
// 当返回的状态码表示没有响应主体时,将响应主体置空:
if (statuses.empty[code]) {
// strip headers
ctx.body = null;
return res.end();
}
// 当请求方法为HEAD时,判断响应头是否发送以及响应主体是否为JSON格式,若满足则设置响应Content-Length:
if ('HEAD' == ctx.method) {
if (!res.headersSent && isJSON(body)) {
ctx.length = Buffer.byteLength(JSON.stringify(body));
}
return res.end();
}
// status body
// 当返回的状态码表示有响应主体,但响应主体为空时,将响应主体设置为响应信息或状态码。并当响应头未发送时设置Content-Type与Content-Length:
if (null == body) {
if (ctx.req.httpVersionMajor >= 2) {
body = String(code);
} else {
body = ctx.message || String(code);
}
if (!res.headersSent) {
ctx.type = 'text';
ctx.length = Buffer.byteLength(body);
}
return res.end(body);
}
// 对不同的响应主体进行处理
// responses
if (Buffer.isBuffer(body)) return res.end(body);
if ('string' == typeof body) return res.end(body);
if (body instanceof Stream) return body.pipe(res);
// body: json
body = JSON.stringify(body);
if (!res.headersSent) {
ctx.length = Buffer.byteLength(body);
}
res.end(body);
}
错误处理
onerror(err) {
// 当err不为Error类型时抛出异常。
if (!(err instanceof Error)) throw new TypeError(util.format('non-error thrown: %j', err));
// 当 err.status 是 404 或 err.expose 是 true 时默认错误处理程序也不会输出错误
if (404 == err.status || err.expose) return;
// 默认情况下,将所有错误输出到 stderr,除非 app.silent 为 true
if (this.silent) return;
const msg = err.stack || err.toString();
console.error();
console.error(msg.replace(/^/gm, ' '));
console.error();
}
第二篇: 图解中间件源码执行过程
中间件
首先写一个简单的中间件demo:
const Koa = require('koa')
const app = new Koa()
const port = 3000
const ctx1 = async (ctx, next) => {
console.log('开始执行中间件1')
await next()
ctx.response.type = 'text/html'
ctx.response.body = '<h3>hello world</h3>'
console.log('结束执行中间件1')
}
app.use(ctx1)
app.use(async function ctx2 (ctx, next) {
console.log('开始执行中间件2')
await next()
console.log('结束执行中间件2')
})
app.listen(port, () => {
console.log(`server is running on the port: ${port}`)
})
很明显中间件执行顺序是这样的:
开始执行中间件1
开始执行中间件2
结束执行中间件2
结束执行中间件1
你可以理解为koa2会先按照中间件注册顺序执行next()之前的代码, 执行完到底部之后, 返回往前执行next()之后的代码。
重点是我们需要koa2源码究竟是怎么样执行的? 现在开始调试模式进入koa2源码一探究竟。
- 首先在两个中间件注册的地方打了断点
- 我们可以看到koa2是先按照你中间件的顺序去注册执行
- 然后会进入callback. 这是因为
// 应用程序
app.listen(port, () => {
console.log(`server is running on the port: ${port}`)
})
// 源码
listen(...args) {
debug('listen');
const server = http.createServer(this.callback());
return server.listen(...args);
}
这个时候this.middleware已经存了两个中间件。
- 这个时候你请求一个路由比如
http://localhost:3000/a
koa2的中间件处理就是在这个函数里面
callback() {
// compose()这是处理中间件的执行顺序所在
}
于是我们进入这个koa-compose的源码看下:
'use strict'
/**
* Expose compositor.
*/
module.exports = compose
/**
* Compose `middleware` returning
* a fully valid middleware comprised
* of all those which are passed.
*
* @param {Array} middleware
* @return {Function}
* @api public
*/
function compose (middleware) {
// 首先是一些中间件格式校验
if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
for (const fn of middleware) {
if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
}
/**
* @param {Object} context
* @return {Promise}
* @api public
*/
return function (context, next) {
// last called middleware #
let index = -1
// 返回一个函数, 从第一个中间件开始执行, 可以通过next()调用后续中间件
return dispatch(0)
// dispatch始终返回一个Promise对象
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即就是通过dispatch(i+1)来执行下一个中间件
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
// 捕获中间件中发生的异常
return Promise.reject(err)
}
}
}
}
此时i=0取出第一个中间件,由于闭包原因i是一直存在的。
这个时候可以看到fn就是ctx1。
注意
// next即就是通过dispatch(i+1)来执行下一个中间件
dispatch.bind(null, i + 1)
这个时候开始进入第一个中间件执行第一句console.log('开始执行中间件1')
这里也能看到next指的就是前面提到的dispatch.bind。
然后我们继续单步调试进入这句
// ctx1中的
await next()
此时又重新进入compose(), 继续执行下一个中间件, i=1
取出第二个中间件函数ctx2。
此时进入第二个中间件ctx2开始执行console.log('开始执行中间件2')
继续单步调试
此时i=2,fx=undefined
// 这个洋葱模型的最后做一个兜底的处理
if (!fn) return Promise.resolve()
执行中间件ctx2的第二句console
补充下async的执行机制: async 的执行机制是:只有当所有的 await 异步都执行完之后才能返回一个 Promise。所以当我们用 async的语法写中间件的时候,执行流程大致如下:
先执行第一个中间件(因为compose会默认执行dispatch(0)),该中间件返回 Promise,然后被Koa监听,执行对应的逻辑(成功或失败)在执行第一个中间件的逻辑时,遇到 await next()时,会继续执行dispatch(i+1),也就是执行 dispatch(1),会手动触发执行第二个中间件。
这时候,第一个中间件 await next() 后面的代码就会被 pending,等待 await next() 返回 Promise,才会继续执行第一个中间件 await next() 后面的代码。
同样的在执行第二个中间件的时候,遇到await next()的时候,会手动执行第三个中间件,await next() 后面的代码依然被 pending,等待 await 下一个中间件的Promise.resolve。
只有在接收到第三个中间件的 resolve 后才会执行后面的代码,然后第二个中间会返回 Promise,被第一个中间件的 await 捕获,这时候才会执行第一个中间件的后续代码,然后再返回 Promise 以此类推。
如果有多个中间件的时候,会依照上面的逻辑不断执行,先执行第一个中间件,在 await next() 出 pending,继续执行第二个中间件,继续在 await next() 出 pending,继续执行第三个中间,直到最后一个中间件执行完,然后返回 Promise,然后倒数第二个中间件才执行后续的代码并返回Promise,然后是倒数第三个中间件,接着一直以这种方式执行直到第一个中间件执行完,并返回 Promise,从而实现文章开头那张图的执行顺序。