2019年9月

经历了一波感觉暗无天日的秋招以后,总算能稍微松口气了,先把自己一直想写的博客,记录一下

源码的入口

首先,可以看到的是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模块

于是我们引入的便属于自定义模块:

自定义模块的查找:

  1. 当前文件目录下的node_modules目录
  2. 父目录下的node_modules目录
  3. 父目录的父目录下的node_modules目录
  4. 沿着路径向上逐级递归,直到根目录的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