zoukankan      html  css  js  c++  java
  • KOA 与 CO 实现浅析

    KOA 与 CO 的实现都非常的短小精悍,只需要花费很短的时间就可以将源代码通读一遍。以下是一些浅要的分析。

    如何用 node 实现一个 web 服务器

    既然 KOA 实现了 web 服务器,那我们就先从最原始的 web 服务器的实现方式着手。
    下面的代码中我们创建了一个始终返回请求路径的 web 服务器。

    const http = require('http');
    const server = http.createServer((req, res) => {
      res.end(req.url);
    });
    server.listen(8001);
    

    当你请求 http://localhost:8001/some/url 的时候,得到的响应就是 /some/url

    KOA 的实现

    简单的说,KOA 就是对上面这段代码的封装。

    首先看下 KOA 的大概目录结构:

    lib 目录下只有四个文件,其中 request.jsresponse.js 是对 node 原生的 request(req)response(res) 的增强,提供了很多便利的方法,context.js 就是著名的上下文。我们暂时抛开这三个文件的细节,先看下主文件 application.js 的实现。

    先关注两个函数:

    // 构造函数    
    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);
    }  
    // listen 方法   
    app.listen = function(){
      debug('listen');
      var server = http.createServer(this.callback());
      return server.listen.apply(server, arguments);
    };
    

    上面的这两个函数,正是完成了一个 web 服务器的建立过程:

    const server = new KOA();  // new Application()
    server.listen(8001);
    

    而先前 http.createServer() 的那个回调函数则被替换成了 app.callback 的返回值。

    我们细看下 app.callback 的具体实现:

    app.callback = function(){
      if (this.experimental) {
        console.error('Experimental ES7 Async Function support is deprecated. Please look into Koa v2 as the middleware signature has changed.')
      }
      var fn = this.experimental
        ? compose_es7(this.middleware)
        : co.wrap(compose(this.middleware));
      var self = this;
    
      if (!this.listeners('error').length) this.on('error', this.onerror);
    
      return function handleRequest(req, res){
        res.statusCode = 404;
        var ctx = self.createContext(req, res);
        onFinished(res, ctx.onerror);
        fn.call(ctx).then(function handleResponse() {
          respond.call(ctx);
        }).catch(ctx.onerror);
      }
    };
    

    先跳过 ES7 的实验功能以及错误处理,app.callback 中主要做了如下几件事情:

    • 重新组合中间件并用 co 包装
    • 返回处理request的回调函数

    每当服务器接收到请求时,做如下处理:

    • 初始化上下文
    • 调用之前 co.wrap 返回的函数,并做必要的错误处理

    现在我们把目光集中到这三行代码中:

    // 中间件重组与 co 包装  
    var fn = co.wrap(compose(this.middleware));
    // ------------------------------------------  
    // 在处理 request 的回调函数中  
    // 创建每次请求的上下文  
    var ctx = self.createContext(req, res);  
    // 调用 co 包装的函数,执行中间件  
    fn.call(ctx).then(function handleResponse() {
      respond.call(ctx);
    }).catch(ctx.onerror);
    

    先看第一行代码,compose 实际上就是 koa-compose,实现如下:

    function compose(middleware){
      return function *(next){
        if (!next) next = noop();
        var i = middleware.length;
        while (i--) {
          next = middleware[i].call(this, next);
        }
        return yield *next;
      }
    }
    function *noop(){}
    

    compose 返回一个 generator函数,这个 generator函数 中倒序依次以 next 为参数调用每个中间件,并将返回的generator实例 重新赋值给 next,最终将 next返回。

    这里比较有趣也比较关键的一点是:

    next = middleware[i].call(this, next);
    

    我们知道,调用 generator函数 返回 generator实例,当 generator函数 中调用其他的 generator函数 的时候,需要通过 yield *genFunc() 显式调用另一个 generator函数

    举个例子:

    const genFunc1 = function* () {
      yield 1;
      yield *genFunc2();
      yield 4;
    }
    const genFunc2 = function* () {
      yield 2;
      yield 3;
    }
    for (let d of genFunc1()) {
      console.log(d);
    }
    

    执行的结果是在控制台依次打印 1,2,3,4。

    回到上面的 compose 函数,其实它就是完成上面例子中的 genFunc1 调用 genFunc2 的事情。而 next 的作用就是保存并传递下一个中间件函数返回的 generator实例

    参考一下 KOA 中间件的写法以帮助理解:

    function* (next) {
      // do sth.
      yield next;
      // do sth.
    }
    

    通过 compose 函数,KOA 把中间件全部级联了起来,形成了一个 generator 链。下一步就是完成上面例子中的 for-of循环的事情了,而这正是 co 的工作。

    co 的原理分析

    还是先看下 co.wrap

    co.wrap = function (fn) {
      createPromise.__generatorFunction__ = fn;
      return createPromise;
      function createPromise() {
        return co.call(this, fn.apply(this, arguments));
      }
    };
    

    该函数返回一个函数 createPromise,也就是 KOA 源码里面的 fn
    当调用这个函数的时候,实际上调用的是 co,只是将上下文 ctx 作为 this 传递了进来。

    现在分析下 co的代码:

    function co(gen) {
      var ctx = this;
      var args = slice.call(arguments, 1)
      // 返回一个 promise
      return new Promise(function(resolve, reject) {
        if (typeof gen === 'function') gen = gen.apply(ctx, args);
        if (!gen || typeof gen.next !== 'function') return resolve(gen);
        
        onFulfilled();
        
        function onFulfilled(res) {
          var ret;
          try {
            ret = gen.next(res);
          } catch (e) {
            return reject(e);
          }
          next(ret);
        }
    
        function onRejected(err) {
          var ret;
          try {
            ret = gen.throw(err);
          } catch (e) {
            return reject(e);
          }
          next(ret);
        }
    
        function next(ret) {
          if (ret.done) return resolve(ret.value);
          var value = toPromise.call(ctx, ret.value);
          if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
          return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
            + 'but the following object was passed: "' + String(ret.value) + '"'));
        }
      });
    }
    

    co 函数的参数是 gen,就是之前 compose 函数返回的 generator实例

    co 返回的 Promise 中,定义了三个函数 onFulfilledonRejectednext,先看下 next 的定义。

    next 的参数实际上就是gen每次 gen.next() 的返回值。如果 gen 已经执行结束,那么 Promise 将返回;否则,将 ret.value promise 化,并再次调用 onFulfilledonRejected 函数。

    onFulfilledonRejected 帮助我们推进 gen 的执行。

    nextonFulfilledonRejected 的组合,实现了 generator 的递归调用。那么究竟是如何实现的呢?关键还要看 toPromise 的实现。

    function toPromise(obj) {
      if (!obj) return obj;
      if (isPromise(obj)) return obj;
      if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj);
      if ('function' == typeof obj) return thunkToPromise.call(this, obj);
      if (Array.isArray(obj)) return arrayToPromise.call(this, obj);
      if (isObject(obj)) return objectToPromise.call(this, obj);
      return obj;
    }  
    

    toPromise 函数中,后三个分支处理分别对 thunk 函数、数组和对象进行了处理,此处略去细节,只需要知道最终都调回了 toPromise 的前三个分支处理中。这个函数最终返回一个 promise 对象,这个对象的 resolvereject 处理函数又分别是上一个 promise 中定义的 onFulfilledonRejected 函数。至此,就完成了 compose 函数返回的 generator 链的推进工作。

    最后还有一个问题需要明确一下,那就是 KOA 中的 context 是如何传递的。
    通过观察前面的代码不难发现,每次关键节点的函数调用都是使用的 xxxFunc.call(ctx) 的方式,这也正是为什么我们可以在中间件中直接通过 this 访问 context 的原因。

  • 相关阅读:
    [zt]petshop4.0 详解之二
    HOW TO: Implement a DataSet JOIN helper class in Visual C# .NET(DataSetHelper)
    DVGPrinter 设置列宽
    [转载]ASP.NET 的学习流程
    初级版FAQ
    [转]PetShop的系统架构设计(1)
    [zt] petshop4.0 详解之三
    mssql2000 jdbc驱动设置
    自动设置环境变量
    Ubuntu中VirtualBox不能使用USB(此法不通)
  • 原文地址:https://www.cnblogs.com/bingooo/p/6005359.html
Copyright © 2011-2022 走看看