Skip to content

Latest commit

 

History

History
executable file
·
268 lines (221 loc) · 7.11 KB

koa源码学习笔记.md

File metadata and controls

executable file
·
268 lines (221 loc) · 7.11 KB

koa源码学习笔记

我们的 koa 程序可以简化为以下三步

const app = new koa();
app.use(middleware);
app.listen(port);

搞清楚这三步分别做了什么?让我们翻开 koa 源码一看究竟.打开 node_modules/koa/package.json,可以看到程序的入口

"main": "lib/application.js"

application.js 主要做了三件事

  1. 创建 app 对象,该对象继承了 Emitter,并且继承了 context,req,res 对象
module.exports = class Application extends Emitter {
  constructor() {
    super();
    this.middleware = []; //中间件数组
    this.context = Object.create(context); //context挂载到app
    this.request = Object.create(request); //request挂载到app
    this.response = Object.create(response); //response挂载到app
  }
};

分别来看看这 lib 下的 context.js, request.js, response.js 三个文件

//  context.js 就暴露了一个对象
const proto = module.exports = {

};

// 使用delegate将request和response对象挂载到proto上
delegate(proto, 'response')  // 这里
  .method('attachment')
  .method('flushHeaders')
  ... 省略若干 .....
  .access('status')
  .getter('writable');

  delegate(proto, 'request') // 这里
  .method('acceptsLanguages')
  .method('acceptsEncodings')
    ... 省略若干 .....
  .access('querystring')
  .getter('ip');

delegate 来自于 node_modules/delegates/index.js,也很简单,就定义了 5 个方法 method, access, getter, setter, fluent

function Delegator(proto, target) {
  this.proto = proto;
  this.target = target;
}
// method, 通过apply将this绑定在target,也就是上面的request和response对象上
Delegator.prototype.method = function (name) {
  var proto = this.proto;
  var target = this.target;
  proto[name] = function () {
    return this[target][name].apply(this[target], arguments);
  };
  return this;
};

request.js 和 response.js:里面都是一系列的 getter setter 方法, 继承自 req 和 res 对象

//request.js
module.exports = {
  get header() {
    return this.req.headers;
  },
  set header(val) {
    this.req.headers = val;
  },
  get url() {
    return this.req.url;
  },
  set url(val) {
    this.req.url = val;
  },
  ... 省略若干 .....
}

因此有

ctx.body = "Hi Allen"; //实际上调用的是response.js里面的set body
  1. use 的时候将回调函数 push 到 middlewares 中间件数组中
  use(fn) {
    this.middleware.push(fn);
    return this;
  }
  1. listen 的时候,创建 http server,监听端口,当端口发生变化时候执行相应的回调,
  listen(...args) {
    const server = http.createServer(this.callback());
    return server.listen(...args);
  }

我们来看下 this.callback,该函数执行的时候返回 handleRequest,handleRequest 首先创建了调用了 createContext,将 req 和 res 上的方法属性挂在了 ctx 上, 其次调用 handleRequest 将 ctx 对象传给中间件函数,

  callback() {
    const fn = compose(this.middleware); //注意这里
    const handleRequest = (req, res) => {
      const ctx = this.createContext(req, res);
      return this.handleRequest(ctx, fn);
    };
    return handleRequest;
  }

这里的 this.context 来自于 context.js 文件,context.js 用引入 delegate.js,因此,原型链查找的方向是 context->context.js 暴露出来的对象->delegate.js 暴露出来的对象, 可以看到 context 可以直接访问 request 和 response 上的属性,也可以通过 context.request 来访问, (至于为什么要有 request.response, response.request 这一步有些疑惑,还望高人指出)

  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;
  }

handleResponse 在中间件执行结束之后再执行

  handleRequest(ctx, fnMiddleware) {
    const res = ctx.res;
    const handleResponse = () => respond(ctx);
    return fnMiddleware(ctx).then(handleResponse).catch(onerror);
  }

去除掉参数校验之后,response 返回客户端的需要的数据

function respond(ctx) {
  const res = ctx.res;
  let body = ctx.body;
  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);
}

中间件的执行顺序问题: 按 use 的顺序执行,碰到 next()就传递控制权,next 执行完之后再返回执行

const koa = require("koa");
const logger = require("koa-logger");
const app = new koa();

const indent = (n) => {
  return new Array(n).join("&nbsp");
};

const mid1 = () => {
  return async (ctx, next) => {
    ctx.body = `<h3>请求 => 第一层中间件</h3>`;
    await next();
    ctx.body += `<h3>响应 => 第一层中间件</h3>`;
  };
};

const mid2 = () => {
  return async (ctx, next) => {
    ctx.body += `<h3>${indent(4)}请求 => 第二层中间件</h3>`;
    await next();
    ctx.body += `<h3>${indent(4)} 响应 => 第二层中间件</h3>`;
  };
};

const mid3 = () => {
  return async (ctx, next) => {
    ctx.body += `<h3>${indent(8)}请求 => 第三层中间件</h3>`;
    await next();
    ctx.body += `<h3>${indent(8)} 响应 => 第三层中间件</h3>`;
  };
};

app.use(logger());
app.use(mid1());
app.use(mid2());
app.use(mid3());
app.use((ctx, next) => {
  ctx.body += `<p style='color: red'>${indent(12)}koa 核心业务处理'</p>`;
});

app.listen(3001);

返回的结果是这样的,类似一种"U 型结构"

请求 => 第一层中间件
   请求 => 第二层中间件
       请求 => 第三层中间件

           koa 核心业务处理'

        响应 => 第三层中间件
    响应 => 第二层中间件
响应 => 第一层中间件

koa 中的中间件之所以有这样的能力,在于 koa-compose 的包装.koa-compose 使用的递归的形式进行调用,并且使用尾递归进行了优化.翻开 koa-compose/index.js 的源码

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);
      }
    }
  };
}

掌握 koa 等框架并不难,难的是 http 协议,资源,请求流程的优化设定,这些属于网络通讯的硬知识,,需要花时间去学习掌握