koa为什么能实现洋葱模型
经历了一波感觉暗无天日的秋招以后,总算能稍微松口气了,先把自己一直想写的博客,记录一下
源码的入口
首先,可以看到的是koa源码中就只有lib目录里面有四个文件:
- application.js
- context.js
- request.js
- response.js
按我们的使用方法:
const Koa = require('koa');
const app = new Koa();
首先第一个疑问就来了:由上面的代码可知require('koa')
得到了koa的构造函数,那么,koa的构造函数是通过哪个文件暴露出来的呢?
换言之,也就是我们阅读koa源码从哪里开始呢?
于是得知道require('koa')
发生了什么,已知require是node的模块机制
require()方法接受一个标识符作为参数。在Node实现中,正是基于这样一个标识符进行模块查找的。模块标识符在Node中主要分为以下几类:
- 核心模块,如http,fs等
- .或..开始的相对路径文件模块
- 以/开始的绝对路径文件模块
- 非路径形式的文件模块,如自定义的connect模块
于是我们引入的便属于自定义模块:
自定义模块的查找:
- 当前文件目录下的node_modules目录
- 父目录下的node_modules目录
- 父目录的父目录下的node_modules目录
- 沿着路径向上逐级递归,直到根目录的node_modules目录
于是进一步揭晓:先到我们的当前文件目录下的node_modules
目录查找,找到了查找的地方,然后呢?
但在文件的定位过程中,还有一些细节需要注意,这主要包括文件拓展名的分析、目录和包的处理
于是首先,koa这个标识符不包含文件拓展名,Node会按.js,.json,.node
的次序补足拓展名依次尝试,但是我们可以看自己的node_modules目录里面koa是一个目录...
此时Node会将目录当作一个包来处理
那么是一个包就会有package.json
,于是:
Node会在当前目录下查找package.json,通过JSON.parse()
解析出包描述对象,从而取出main属性指定的文件名进行定位
如果文件名缺少扩展名,则进入扩展名分析环节
如果文件名错误,或者没有package.json,则Node会把index当做默认文件名,然后依次查找index.js,index.json,index.node
...
注意这里如果package.json没有main属性,应该也是直接使用index当做默认文件名,例如koa-compose
的包:
- History.md
- index.js(直接在index中
module.exports = compose
了) - package.json
- Readme.md(没有main属性)
回到koa源码中,正好node_modules的koa目录下的package.json
中有main
属性:
"main": "lib/application.js"
然后再去application.js中去查找:module.exports = class Application extends Emitter { // code }
答案揭晓:通过require('koa')
得到的Koa构造函数,其实是lib/application.js
中暴露出来的class Application
!
于是,终于走了小小的一步...
参考:《深入浅出Node.js》
开始探索
我们知道了const Koa = require('koa');
,中的Koa
代表的是,class Application
,接下来回到我们要探索的问题:为什么koa会表现成一个洋葱模型?
如下:
const Koa = require("koa");
const app = new Koa();
app.use(async (ctx, next) => {
console.log(1);
await next();
console.log(2);
});
app.use(async (ctx, next) => {
console.log(3);
await next();
console.log(4);
});
app.use(ctx => {
ctx.body = "Hello Koa";
});
app.listen(3000);
console.log('server start: 3000');
// output:
// 1
// 3
// 4
// 2
所以我们开始从application.js中寻找答案,首先示例代码中使用了use
,我们可以从application.js中去找到use
方法:
use(fn) {
// 边界判断
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
// 如注释所示,为了转化generator
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;
}
由于构造函数中,this.middleware = [];
,得知应该是把fn放入数组中存储起来
于是这里use
函数就结束了,然后就到了app.listen(3000)
,接着看listen
方法
listen(...args) {
debug('listen');
// 使用了http模块的createServer与Appalication的callback方法
const server = http.createServer(this.callback());
return server.listen(...args);
}
然后我们再看callback
方法:
callback() {
// 把之前存储函数middleware,传给了compose函数?
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;
}
// todo