资讯专栏INFORMATION COLUMN

Koa源码阅读笔记(3) -- 服务器の启动与请求处理

mrcode / 1175人阅读

摘要:本笔记共四篇源码阅读笔记源码阅读笔记源码阅读笔记服务器启动与请求处理源码阅读笔记对象起因前两天阅读了的基础,和中间件的基础。的前端乐园原文链接源码阅读笔记服务器启动与请求处理

本笔记共四篇
Koa源码阅读笔记(1) -- co
Koa源码阅读笔记(2) -- compose
Koa源码阅读笔记(3) -- 服务器の启动与请求处理
Koa源码阅读笔记(4) -- ctx对象

起因

前两天阅读了Koa的基础co,和Koa中间件的基础compose
然后这两天走在路上也在思考一些Koa运行机制的问题,感觉总算有点理通了。
今天就来解读一下Koa启动时,发生的一系列事情。

启动

如果只是单纯用Koa,那么启动服务器是很方便的。
下面就是一个最简单的Hello World的例子。

var koa = require("koa")
var app = new koa()

app.use(function * (next) {
  this.set("Powered by", "Koa2-Easy")
  yield next
})

app.use(function * (next) {
  this.body = "Hello World!"
})

app.listen(3000) 

在上一节对koa-compose的分析中,解决了我一个问题,那就是使用中间件时,那个next参数是如何来的。
这一节也会解决一个问题,那就是中间件中的this是如何来的。

有意思的地方 无new也可使用的构造函数

首先看Koa构造函数的源代码:

/**
 * Expose `Application`.
 */

module.exports = Application;

/**
 * Initialize a new `Application`.
 *
 * @api public
 */

function Application() {
  if (!(this instanceof Application)) return new Application;
  this.env = process.env.NODE_ENV || "development";
  this.subdomainOffset = 2;
  this.middleware = [];
  this.proxy = false;
  this.context = Object.create(context);
  this.request = Object.create(request);
  this.response = Object.create(response);
}

Application函数内部的第一句很有意思。

if (!(this instanceof Application)) return new Application;

因为是构造函数,但很多人会忘记使用new来初始化。但是在Koa,则做了一点小措施,从而达到了是否调用new都能初始化的效果。

原型的写法

关于原型的写法,很多人肯定不陌生。以Koa的Application为例,平时如果要写原型的属性,那么会是这样写的。

function Application() {}
Application.prototype.listen = function () {}
Application.prototype.callback = function () {}

这样写的话,每次都需要写冗长的Application.prototype
而在Koa中,则使用一个变量,指向了prototype

var app = Application.prototype;
app.listen = function () {}
app.callback = function () {}

写起来简洁,看起来也简洁。

服务器の启动流程

在Koa中,或者说一切Node.js的Web框架中,其底层都是Node.js HTTP模块来构建的服务器。
那么我就对这点产生了好奇,到底是什么,能让发送给服务器的相应,被Koa等框架截获,并进行相应处理。
同时在Koa框架中,调用listen方法才能启动服务。
那么服务器的启动流程就从listen方法开始。

启动服务器

首先是listen方法的源代码

/**
 * Shorthand for:
 *
 *    http.createServer(app.callback()).listen(...)
 *
 * @param {Mixed} ...
 * @return {Server}
 * @api public
 */

app.listen = function(){
  debug("listen");
  var server = http.createServer(this.callback());
  return server.listen.apply(server, arguments);
};

不难看出,只有使用了listen方法,http服务才会被真正的创建并启动。
而查阅文档,则看到在http.createServer(this.callback())中传入的参数的作用。

在这里,server 每次接收到请求,就会将其传入回调函数处理。
同时listen方法执行完毕时,server便开始监听指定端口。
所以在这里,callback便成为一个新的重点。

处理响应

继续放上callback的源代码(删除部分无用部分):

/**
 * Return a request handler callback
 * for node"s native http server.
 *
 * @return {Function}
 * @api public
 */

app.callback = function(){
  var fn = co.wrap(compose(this.middleware));
  var self = this;

  if (!this.listeners("error").length) this.on("error", this.onerror);

  return function(req, res){
    res.statusCode = 404;
    var ctx = self.createContext(req, res);
    onFinished(res, ctx.onerror);
    fn.call(ctx).then(function () {
      respond.call(ctx);
    }).catch(ctx.onerror);
  }
};

在这儿,Koa的注释对这个函数的作用解释的很清楚。

Return a request handler callback for node"s native http server.

而这儿,对于闭包的应用则让我眼前一亮。
由于服务器启动后,中间件是固定的,所以像初始化中间件,保持this引用,注册事件这种无需多次触发或者高耗能事件,便放入闭包中好了。
一次创建,多次使用。

说到这儿想起一个问题,上次NodeParty, Koa演讲结束后,有人询问Koa能否根据请求做到动态加载中间件,当时他没回答出来。
就源代码来看,是不能做到动态加载的。最多也只是在中间件内部做一些判断,从而决定是否跳过。

往下继续读,则可以看到这一行:

var ctx = self.createContext(req, res);

在context中,是把一些常用方法挂载至ctx这个对象中。
比如在koa中,直接调用this.body = "Hello World"这种response的方法,或者通过this.path获得request的路径都是可行的。
而不用像Express一般,requestresponse方法泾渭分明。同时在使用过程中,是明显有感觉到KoaExpress要便利的。而不仅仅是解决回调地狱那么简单。

中间件的处理

在第一节Koa源码阅读笔记(1) -- co中,已经解释了co.wrap的作用。
这儿可以再看一次compose函数的源代码。

function compose(middleware){
  return function *(next){
    // next不存在时,调用一个空的generator函数
    if (!next) next = noop();

    var i = middleware.length;
    // 倒序处理中间件,给每个中间件传入next参数
    // 而next则是下一个中间件
    while (i--) {
      next = middleware[i].call(this, next);
    }

    return yield *next;
  }
}

function *noop(){}

在这里,中间件被倒序处理,保证第一个中间件的next参数为第二个中间件函数,第二个的next参数则为第三个中间件函数。以此类推。
而最后一个则以一个空的generator函数结尾。

在这儿,有想了很久才想通的点,那就是next = middleware[i].call(this, next);时,middleware没有返回值,为什么next参数等于下一个函数。
到后来才想通,中间件都是generator函数。generaotr会返回一个指向内部状态的指针对象。
这一点我在co的阅读笔记用提及, 也在阮一峰的《ECMAScript 6入门》看到了。

不同的是,调用Generator函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象。需要手动调用它的next()方法。

但当时就是想不起来,结果睡了一觉就突然领悟了。= =
最近也在上一门课,名称就叫《学习如何学习》,里面也有提到睡眠能帮自己整理记忆,遇到问题也不需要死钻牛角尖,说不定过一会儿答案会自己浮现的。

目前来看,确实是说的很对。

同时在compose函数最后的部分,返回了一个yield *next;

通过翻阅 《ECMAScript 6入门》-- 可知。

如果在Generater函数内部,调用另一个Generator函数,默认情况下是没有效果的。这个就需要用到yield*语句,用来在一个Generator函数里面执行另一个Generator函数。

也就是说,其实每次执行时,是这样的:

co(function* (next) {
  if (!next) next = noop();

  var i = middleware.length;

  while (i--) {
    next = middleware[i].call(this, next);
  }

  return yield *next;
})

return yield *next, next作为第一个中间件,会被执行。
如果碰到中间件中的next,则会被co继续调用和执行。
因为在co中,碰到generator函数是这样的:

if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj);

当然,如果在某个中间件中,碰到了以yield形式调用的函数,则会按co的规则,一路调用下去。
当中间件调用时,会返回一个Promise,而Promise在co中,会通过onFulfilled函数,实现自动调用。
从而就形成了独特的Koa风格。

有点迷糊的话,举个具体的栗子:

var koa = require("koa")
var app = new koa()

app.use(function * (next) {
  console.log("middleware 1 start")
  yield next
  console.log("middleware 1 finished")
})

app.use(function * (next) {
  console.log("middleware 2 finished")
})

app.listen(3000) 

当接收到响应时,首先输出middleware 1 start,然后碰到了 yield next, next是下一个中间件,会被co处理为Promise函数。
而当第二个中间件执行完毕时,Promise自动调用then函数,而then却又是第一个中间件的onFulfilled函数。
那么第一个中间件就会继续向下执行。直到执行完成。

所以最后Koa的接收响应并处理的图,是这样的:

中间件中的this

到这一步,这些东西就好解释了。

var ctx = self.createContext(req, res);
onFinished(res, ctx.onerror);
fn.call(ctx).then(function () {
  respond.call(ctx);
}).catch(ctx.onerror);

fn是处理过的中间件函数,使用call将创建好的ctx对象作为this传入,就可以实现在中间件中使用this来处理请求/响应。

其他

在整个处理过程中,心细的小伙伴还注意到了onFinished函数和respond函数。
onFinished函数是一个Node的模块。地址。
作用则是在请求结束或错误是自动调用。所以这儿把ctx.onerror这个错误处理函数传入,防止请求就直接是错的。

而respond则是koa内部的函数,用于处理在中间件内部经过处理的ctx对象,并发送响应。
至此,Koa的启动和响应流程便完整的走了一遍。

结语

有些感慨,也有些唏嘘。
有很多想说的,但也感觉没什么可说的。
就这样吧。

前端路漫漫,且行且歌。

最后附上本人博客地址和原文链接,希望能与各位多多交流。

Lxxyx的前端乐园
原文链接:Koa源码阅读笔记(3) -- 服务器の启动与请求处理

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/90862.html

相关文章

  • Koa源码阅读笔记(4) -- ctx对象

    摘要:本笔记共四篇源码阅读笔记源码阅读笔记源码阅读笔记服务器启动与请求处理源码阅读笔记对象起因前两天终于把自己一直想读的源代码读了一遍。首先放上关键的源代码在上一篇源码阅读笔记服务器启动与请求处理中,我们已经分析了的作用。 本笔记共四篇Koa源码阅读笔记(1) -- coKoa源码阅读笔记(2) -- composeKoa源码阅读笔记(3) -- 服务器の启动与请求处理Koa源码阅读笔记(4...

    ityouknow 评论0 收藏0
  • Koa源码阅读笔记(2) -- compose

    摘要:于是抱着知其然也要知其所以然的想法,开始阅读的源代码。问题读源代码时,自然是带着诸多问题的。源代码如下在被处理完后,每当有新请求,便会调用,去处理请求。接下来会继续写一些阅读笔记,因为看的源代码确实是获益匪浅。 本笔记共四篇Koa源码阅读笔记(1) -- coKoa源码阅读笔记(2) -- composeKoa源码阅读笔记(3) -- 服务器の启动与请求处理Koa源码阅读笔记(4) -...

    roland_reed 评论0 收藏0
  • Koa源码阅读笔记(1) -- co

    摘要:正好自己之前也想看的源代码,所以趁着这个机会,一口气将其读完。源码解读的源代码十分简洁,一共才两百余行。结语的源代码读取来不难,但其处理方式却令人赞叹。而且阅读的源代码,是阅读源码的必经之路。 本笔记共四篇Koa源码阅读笔记(1) -- coKoa源码阅读笔记(2) -- composeKoa源码阅读笔记(3) -- 服务器の启动与请求处理Koa源码阅读笔记(4) -- ctx对象 起...

    taoszu 评论0 收藏0
  • Koa2源码阅读笔记

    摘要:引言最近空闲时间读了一下的源码在阅读的源码的过程中,我的感受是代码简洁思路清晰不得不佩服大神的水平。调用的时候就跟有区别使用必须使用来调用除了上面的的构造函数外,还暴露了一些公用的,比如两个常见的,一个是,一个是。 引言 最近空闲时间读了一下Koa2的源码;在阅读Koa2(version 2.2.0)的源码的过程中,我的感受是代码简洁、思路清晰(不得不佩服大神的水平)。下面是我读完之后...

    plus2047 评论0 收藏0
  • koa源码阅读[1]-koakoa-compose

    摘要:接上次挖的坑,对相关的源码进行分析第一篇。和同为一批人进行开发,与相比,显得非常的迷你。在接收到一个请求后,会拿之前提到的与来创建本次请求所使用的上下文。以及如果没有手动指定,会默认指定为。 接上次挖的坑,对koa2.x相关的源码进行分析 第一篇。 不得不说,koa是一个很轻量、很优雅的http框架,尤其是在2.x以后移除了co的引入,使其代码变得更为清晰。 express和ko...

    vibiu 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<