Koa源码分析

419 阅读4分钟

一.下载koa源代码

先看看这个极简的启动代码:

const Koa = require('koa');
const app = new Koa();

// response
app.use(ctx => {
  ctx.body = 'Hello Koa';
});

app.listen(3000);

我们在koa源码文件夹下创建index.js文件, 将上面的代码写入,并将require('koa') 换成 require('.')

const Koa = require('.')
debugger
const app = new Koa();
app.use(ctx => {
  ctx.body = 'Hello Koa';
});
app.listen(3000);

然后进入目录运行node --inspect-brk index.js

在chrome浏览器打开调试 chrome://inspect

   1.引入Koa

  • require('.')经过路径分析

require 会依照这个顺序去查找需要的文件

Module._extensions={
    .js:funciton(module,filename),
    .json:funciton(module,filename),
    .node:funciton(module,filename)
}

通过读取package.json中的main字段得到完整路径

  • 路径分析之后,是文件定位

查找到路径之后通过 fs.readFileSync加载模块

  • 编译执行

读取文件后开始编译,首先将读取的代码script,进行组装,

即头部添加(function (exports, require, module, __filename, __dirname) { ',

尾部添加

'\n});'

NativeModule.wrap = function(script) {
    return NativeModule.wrapper[0] + script + NativeModule.wrapper[1];
  };

NativeModule.wrapper = [
    '(function (exports, require, module, __filename, __dirname) { ',
    '\n});'
  ];

这就是为什么每个模块没有定义exports, require, module, __filename, __dirname变量,却能使用的原因

2. 创建Koa对象

Application

constructor()

首先new Koa()的时候就是new的这个对象, 最关键的是创建了context,request,response对象

  constructor() {
    super();

    this.proxy = false;
    // 中间件初始化为一个列表
    this.middleware = [];

    this.subdomainOffset = 2;
    // 默认为开发环境
    this.env = process.env.NODE_ENV || 'development';
    // 创建Context, Request,Response对象,为什么要用Object.create函数呢?
    this.context = Object.create(context);
    this.request = Object.create(request);
    this.response = Object.create(response);
  }
use()

通过该方法引入中间件,中间件可以是一个普通的函数, 可以是一个generator, 可以是async函数,如果是generator那么koa会帮你转成基于promise的函数。根据use使用的先后顺序, middleware数组中会存储所有的中间件。

  use(fn) {
     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);
    }
    this.middleware.push(fn);
    return this;
  }
listen()

listen函数表面上看是用于监听某个端口,实际上包裹了真正的nodejs提供的http server, createServer函数接受一个处理请求的函数,由callback()返回

  listen(...args) {
    debug('listen');
    const server = http.createServer(this.callback());
    return server.listen(...args);
  }
callback()

callback函数是Application中真正核心的处理逻辑, 其返回了一个请求处理器,封装了koa所有的方法逻辑

callback() {
    // compose函数将注册的中间件组合成了一个函数,类似于递归
    const fn = compose(this.middleware);
    // Koa Application 扩展了 Emitter类, listeners 是Emitter类的属性.
    // 这里表示若没有注册error事件处理函数, 则注册一个
    if (!this.listeners('error').length) this.on('error', this.onerror);
    
    // 返回的请求处理函数, req, res是createServer回调时会传入的nodejs请求和响应对象。
    const handleRequest = (req, res) => {
      // 默认的的状态码为404
      res.statusCode = 404;
      // 创建koa应用的上下文, context将很多属性和方法都代理到这个对象上方便开发.
      const ctx = this.createContext(req, res);
      // 使用 ctx.onerror处理请求错误, 详见Context
      const onerror = err => ctx.onerror(err);
      // 处理响应函数
      const handleResponse = () => respond(ctx);
      // 请求完成之后如果出错调用onerror
      onFinished(res, onerror);
      // 等中间件都处理完了之后处理响应
      return fn(ctx).then(handleResponse).catch(onerror);
    };

    return handleRequest;
  }

compose 组合所有的中间件,返回一个高阶函数,每次调用next index+1,调用对应的中间件

函数,形成递归调用,洋葱模型

function compose (middleware) {

  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 {
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}
createContext()

这个函数与其说是创建Context对象,不如说是将koa的各个内部对象连接在一起。并设置cookies和accepts.

// req, res 是node的原生请求响应对象,是所有信息的来源.
// request,response是koa提供的方便我们开发使用的请求响应对象.
  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;
    // originalUrl即 req.url
    context.originalUrl = request.originalUrl = req.url;
    // cookies 直接使用的第三方库
    context.cookies = new Cookies(req, res, {
      keys: this.keys,
      secure: request.secure
    });
    // 更常用的从ip中读取请求方的IP地址, ips是?
    request.ip = request.ips[0] || req.socket.remoteAddress || '';
    // 使用accepts设置请求能接受的内容类型
    context.accept = request.accept = accepts(req);
    // ?
    context.state = {};
    return context;
  }
respond()

当我们完成处理需要返回时我们设置this.body = 'xxx' 然后函数返回,不需要我们手动调用res.end(),因为koa已经帮我们封装好了.

/**
 * Response helper.
 */

function respond(ctx) {
  // ...
  // 如果HTTP状态码表示内容应该为空,则设空返回
  if (statuses.empty[code]) {
    
    ctx.body = null;
    return res.end();
  }
  
  if ('HEAD' === ctx.method) {
    // 要求返回响应头,如果headersSent为false
    if (!res.headersSent && isJSON(body)) {
      ctx.length = Buffer.byteLength(JSON.stringify(body));
    }
    return res.end();
  }

  // 如果没有设置body,只设置了status,则用状态码或message设置body.
  if (null == body) {
    body = ctx.message || String(code);
    if (!res.headersSent) {
      ctx.type = 'text';
      ctx.length = Buffer.byteLength(body);
    }
    return res.end(body);
  }
    
  // koa 支持Buffer, string, Stream类型的数据
  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 处理普通json类型返回.
  body = JSON.stringify(body);
  if (!res.headersSent) {
    ctx.length = Buffer.byteLength(body);
  }
  res.end(body);
}

Context

Context是整个请求的上下文,其最特殊的地方其实是整合response和request,让你在应用的任何地方通过context获得应用相关的任何信息

Response

响应体相关的方法属性