以API为线索来理解koa源码

代码结构:

整个模块以applacation文件为主,其余context、request、response三个文件返回三个对象供applacation文件调用

application.js:

  • Class Applacation
    • constructor()
    • listen()
    • callback()
    • handleRequest()
    • createContext()
    • inspect()
    • onerror()
    • toJson()
    • use()
  • respond()
以app.listen函数为线索:
const Koa = require("koa")
const app = new Koa()

app.listen(3000)

这段代码在Koa中运行情况如下:

  • new Koa()是创建一个新的application类的实例,这就会首先执行application类的constructor方法,执行完就使得app实例获得了一些属性和方法,如listen。

  • app.listen内部执行情况:

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

callback() {
    //从这里将中间件数组打包为一个包含所有中间件的函数
    const fn = compose(this.middleware);
    // 检查error事件的listener个数,如果为0则添加一个error的listener:this.onerror
    if (!this.listenerCount('error')) this.on('error', this.onerror);
    // 返回一个函数去处理req和res,因为这个函数要放到http.createServer里面作为回调函数去处理请求
    const handleRequest = (req, res) => {
      const ctx = this.createContext(req, res);//得到一个新的context,该context已经完成了一些基本属性的创建
      return this.handleRequest(ctx, fn); 
    };

    return handleRequest; //也就是说每次请求进来都执行的是这个handleRequest函数
  }

子函数:

//application.js
 handleRequest(ctx, fnMiddleware) {
   console.log('handleRequest');
   const res = ctx.res;
   res.statusCode = 404; // 设置返回状态码为404,即404是默认的
   const onerror = err => ctx.onerror(err);
   const handleResponse = () => respond(ctx);// 定义handleResponse函数在其中调用respond函数,作用为绑定respond调用时的参数ctx
   onFinished(res, onerror);
   return fnMiddleware(ctx).then(handleResponse).catch(onerror); // 在这里先执行middleware然后执行handleResponse
 }


function respond(ctx) {
 // allow bypassing koa   当ctx.respond或者ctx.writable是false的时候koa不处理response
 if (false === ctx.respond) return;

 if (!ctx.writable) return;

 const res = ctx.res;
 let body = ctx.body;
 const code = ctx.status;

 // ignore body   通过statuses.empty[code]来判断该状态码是否有返回主体,如3xx,4xx,5xx就没有返回主体
 if (statuses.empty[code]) {
   // strip headers
   ctx.body = null;
   return res.end();
 }

 //当请求方法是HEAD的时候,不需要返回实体内容
 if ('HEAD' == ctx.method) {
   if (!res.headersSent && isJSON(body)) {  //res.headersSent是http模块的api,用于检查是否已经发送过header,如果已经发送就不再发送
     ctx.length = Buffer.byteLength(JSON.stringify(body)); // 设置response的length
   }
   return res.end();
 }

 // status body 当body是null的时候
 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  真正的往客户端响应,当body的类型是buffer或者string的时候就直接返回,是stream的时候使用pipe
 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);
}

以上就是一个app.listen的流程,接下来还有一个重点就是app.use

以app.use函数为线索
//application.js
  use(fn) {
    if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
    if (isGeneratorFunction(fn)) { // 如果是调用了generator函数,则发出弃用警告,警告内容如下
      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); // 将generator函数转化为promise
    }
    debug('use %s', fn._name || fn.name || '-');
    this.middleware.push(fn);
    return this;
  }
 //这个函数最主要的操作就是this.middleware.push(fn),this.middleware究竟是什么就要看constructor构造函数了
 
   constructor() {
    super();

    this.proxy = false;
    this.middleware = [];  // 揭示了中间件的本质实际上是一个函数调用,在内部仅仅调用了第一个函数,只有在每个函数中调用next才会调用下一个中间件
    this.subdomainOffset = 2;
    this.env = process.env.NODE_ENV || 'development';
    this.context = Object.create(context); // ctx就是根据app.context来创建的,因此,如果想要拓展ctx的内容,就需要修改app.context
    this.request = Object.create(request);
    this.response = Object.create(response);
    if (util.inspect.custom) {  // 如果用户自定义了util.inspect那就用自己的inspect代替他的
      this[util.inspect.custom] = this.inspect;
    }
  }
 // 所以我们就可以发现app.use其实就是再往middleware数组中push了一个函数。
 

在上述callback函数中我们可以发现 const fn = compose(this.middleware) 这行代码。通过compose可以将middleware这个函数数组转换为一个包含了数组中所有函数的Promise函数,然后通过this.handleRequest(ctx, fn)这行代码将返回的Promise函数传给下一个函数,在handleRequest中有fnMiddleware(ctx).then(handleResponse).catch(onerror),将ctx传给返回的Promise函数就完成了对第一个中间件的调用,如果想调用下一个中间件就必须再上一个中间件中调用next函数,next函数实际上就是一个绑定了调用顺序的Promise函数。

关于compose的具体内容请见https://blog.csdn.net/qq_39807732/article/details/91129409

以ctx.body=为线索

首先我们要找到body在context中的定义从哪来的:

//application.js
  createContext(req, res) {
    const context = Object.create(this.context);// 创建context继承自context
    const request = context.request = Object.create(this.request);// 创建context.request继承自request
    const response = context.response = Object.create(this.response);// 创建context.response继承自reponse
    context.app = request.app = response.app = this; // 创建context.app指向this
    context.req = request.req = response.req = req; // context.req指向http.createServer传入的req
    context.res = request.res = response.res = res; // context.res指向http.createServer传入的res
    request.ctx = response.ctx = context; // context.ctx指向context
    request.response = response;
    response.request = request;
    context.originalUrl = request.originalUrl = req.url; // context.originalUrl指向req.url
    context.state = {}; // context.state创建为空对象
    return context;
  }

回顾上面的callback函数,其中const ctx = this.createContext(req, res);说明了ctx就是从createContext这个函数创建而来的,但这个函数中并没有定义body属性,但是可以发现context是从context.js文件暴露出来的对象继承来的,所以body属性应当定义在context.js中

//application.js
delegate(proto, 'response')
  .method('attachment')
  .method('redirect')
  .method('remove')
  .method('vary')
  .method('set')
  .method('append')
  .method('flushHeaders')
  .access('status')
  .access('message')
  .access('body') // body属性从这里来
  .access('length')
  .access('type')
  .access('lastModified')
  .access('etag')
  .getter('headerSent')
  .getter('writable');

这个delegate模块的作用就是实现了设计模式当中的委托模式(Delegation Pattern),道理很简单,就是将proto对象的response属性的属性设置到proto上。关于delegate模块具体可以参考这里

我们知道context的response属性就是根据response.js所暴露出来对象创建的,所以说body属性定义在了response.js中。

//esponse.js
 get body() {
    return this._body;
 },
 set body(val) {
    const original = this._body;
    this._body = val;

    // no content
    if (null == val) {
      if (!statuses.empty[this.status]) this.status = 204;
      this.remove('Content-Type');
      this.remove('Content-Length');
      this.remove('Transfer-Encoding');
      return;
    }

    // set the status
    if (!this._explicitStatus) this.status = 200;

    // set the content-type only if not yet set
    const setType = !this.header['content-type'];

    // string
    if ('string' == typeof val) {
      if (setType) this.type = /^\s*</.test(val) ? 'html' : 'text';
      this.length = Buffer.byteLength(val);
      return;
    }

    // buffer
    if (Buffer.isBuffer(val)) {
      if (setType) this.type = 'bin';
      this.length = val.length;
      return;
    }

    // stream
    if ('function' == typeof val.pipe) {
      onFinish(this.res, destroy.bind(null, val));
      ensureErrorHandler(val, err => this.ctx.onerror(err));

      // overwriting
      if (null != original && original != val) this.remove('Content-Length');

      if (setType) this.type = 'bin';
      return;
    }

    // json
    this.remove('Content-Length');
    this.type = 'json';
  },

可以看出body属性是通过get/set设置的,其内部将body真正的值设置在了_body上,而_body是在set body的时候添加到this上的。我们可以看到set body其实就是完成了对this._body的赋值,以及对传入的val的类型验证,通过val的类型去设置headers。注意:这里并没有进行res.end.

所以当我们使用ctx.body=的时候其实就是一个普通的赋值操作。那究竟是在哪里返回的连接呢。请注意application.js中的listen流程。

在listen流程中我们可以发现:当一个http请求到来时先执行了所有的中间件,最后再执行respond函数,而res.end也是在此时进行的。

然后我们思考:当我们在中间件中自己调用了ctx.res.end会怎么样呢?我们可以发现在respond函数中,每次res.end之前都会检查headersSent, 如果还没有返回链接才回调用res.end

最后

最近读了一些npm模块的源码,有一些阅读源码的小感受:

在阅读源码的时候,最好是结合自己常用的API,以某个API为线索寻找该API的实现流程,在完成几条线索之后整个模块的实现思路就会清晰起来。这样的方式也更符合一般人的思维流程和知识的结构化。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值