zoukankan      html  css  js  c++  java
  • koa源码


    koa1

    参考代码: learn-koa2

    我们先来看段原生Node实现Server服务器的代码:

    const http = require('http');
    
    const server = http.createServer((req, res) => {
      res.writeHead(200);
      res.end('hello world');
    });
    
    server.listen(3000, () => {
      console.log('server start at 3000');
    });
    

      

    非常简单的几行代码,就实现了一个服务器Server。createServer方法接收的callback回调函数,可以对每次请求的req res对象进行各种操作,最后返回结果。不过弊端也很明显,callback函数非常容易随着业务逻辑的复杂也变得臃肿,即使把callback函数拆分成各个小函数,也会在繁杂的异步回调中渐渐失去对整个流程的把控。 

    另外,Node原生提供的一些API,有时也会让开发者疑惑:

    res.statusCode = 200;
    res.writeHead(200);
    

      

    修改res的属性或者调用res的方法都可以改变http状态码,这在多人协作的项目中,很容易产生不同的代码风格。 

    我们再来看段Koa实现Server:

    const Koa = require('koa');
    const app = new Koa();
    
    app.use(async (ctx, next) => {
      console.log('1-start');
      await next();
      console.log('1-end');
    });
    
    
    app.use(async (ctx, next) => {
      console.log('2-start');
      ctx.status = 200;
      ctx.body = 'Hello World';
      console.log('2-end');
    });
    
    
    app.listen(3000);
    
    // 最后输出内容:
    // 1-start
    // 2-start
    // 2-end
    // 1-end
    

      


    Koa使用了中间件的概念来完成对一个http请求的处理,同时,Koa采用了async和await的语法使得异步流程可以更好的控制。ctx执行上下文代理了原生的resreq,这让开发者避免接触底层,而是通过代理访问和设置属性。 

    看完两者的对比后,我们应该会有几个疑惑:

    • ctx.status为什么就可以直接设置状态码了,不是根本没看到res对象吗?
    • 中间件中的next到底是啥?为什么执行next就进入了下一个中间件?
    • 所有中间件执行完成后,为什么可以再次返回原来的中间件(洋葱模型)?

    现在让我们带着疑惑,进行源码解读,同时自己实现一个简易版的Koa吧!

    封装http Server

    参考代码: step-1

    // Koa的使用方法
    const Koa = require('koa');
    const app = new Koa();
    
    app.use(async ctx => {
      ctx.body = 'Hello World';
    });
    
    app.listen(3000);
    

      

    我们首先模仿koa的使用方法,搭建一个最简易的骨架: 

    新建kao/application.js(特意使用了Kao,区别Koa,并非笔误!!!)

    const http = require('http');
    
    class Application {
      constructor() {
        this.callbackFn = null;
      }
    
      use(fn) {
        this.callbackFn = fn;
      }
    
      callback() {
        return (req, res) => this.callbackFn(req, res)
      }
    
      listen(...args) {
        const server = http.createServer(this.callback());
        return server.listen(...args);
      }
    }
    
    module.exports = Application;
    

      

    新建测试文件kao/index.js 

    const Kao = require('./application');
    const app = new Kao();
    
    app.use(async (req, res) => {
      res.writeHead(200);
      res.end('hello world');
    });
    
    app.listen(3001, () => {
      console.log('server start at 3001');
    });
    

      

    我们已经初步封装好http server:通过new实例一个对象,use注册回调函数,listen启动server并传入回调。 

    注意的是:调用new时,其实没有开启server服务器,真正开启是在listen调用时。

    不过这段代码有明显的不足:

    • use传入的回调函数,接收的参数依旧是原生的reqres
    • 多次调用use,会覆盖上一个中间件,并不是依次执行多个中间件

    我们先来解决第一个问题

    封装req和res对象,构造context

    参考代码: step-2

    先来介绍下ES6中的get和set 参考

    基于普通对象的get和set

    const demo = {
      _name: '',
      get name() {
        return this._name;
      },
    
      set name(val) {
        this._name = val;
      }
    };
    
    demo.name = 'deepred';
    console.log(demo.name);
    

      

    基于Class的get和set 

    class Demo {
      constructor() {
        this._name = '';
      }
    
      get name() {
        return this._name;
      }
    
      set name(val) {
        this._name = val;
      }
    }
    
    const demo = new Demo();
    demo.name = 'deepred';
    console.log(demo.name);
    

      

    基于Object.defineProperty的get和set 

    const demo = {
      _name: ''
    };
    
    Object.defineProperty(demo, 'name', {
      get: function() {
        return this._name
      },
    
      set: function(val) {
        this._name = val;
      }
    });
    

      

    基于Proxy的get和set 

    const demo = {
      _name: ''
    };
    
    const proxy = new Proxy(demo, {
      get: function(target, name) {
        return name === 'name' ? target['_name'] : undefined;
      },
    
      set: function(target, name, val) {
        name === 'name' && (target['_name'] = val)
      }
    });
    

      

    还有__defineSetter____defineGetter__的实现,不过现已废弃。 

    const demo = {
      _name: ''
    };
    
    demo.__defineGetter__('name', function() {
      return this._name;
    });
    
    demo.__defineSetter__('name', function(val) {
      this._name = val;
    });
    

      

    主要区别是,Object.defineProperty __defineSetter__ Proxy可以动态设置属性,而其他方式只能在定义时设置。 

    Koa源码中 request.jsresponse.js就使用了大量的getset来代理

    新建kao/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;
      },
    }
    

      

    当访问request.url时,其实就是在访问原生的req.url。需要注意的是,this.req原生对象此时还没有注入! 

    同理新建kao/response.js

    module.exports = {
      get status() {
        return this.res.statusCode;
      },
    
      set status(code) {
        this.res.statusCode = code;
      },
    
      get body() {
        return this._body;
      },
    
      set body(val) {
        // 源码里有对val类型的各种判断,这里省略
        /* 可能的类型
        1. string
        2. Buffer
        3. Stream
        4. Object
        */
        this._body = val;
      }
    }
    

      

    这里对body进行操作并没有使用原生的this.res.end,因为在我们编写koa代码的时候,会对body进行多次的读取和修改,所以真正返回浏览器信息的操作是在application.js里进行封装和操作 

    同样需要注意的是,this.res原生对象此时还没有注入!

    新建kao/context.js

     

    const delegate = require('delegates');
    
    const proto = module.exports = {
      // context自身的方法
      toJSON() {
        return {
          request: this.request.toJSON(),
          response: this.response.toJSON(),
          app: this.app.toJSON(),
          originalUrl: this.originalUrl,
          req: '<original node req>',
          res: '<original node res>',
          socket: '<original node socket>'
        };
      },
    }
    
    // delegates 原理就是__defineGetter__和__defineSetter__
    
    // method是委托方法,getter委托getter,access委托getter和setter。
    
    // proto.status => proto.response.status
    delegate(proto, 'response')
      .access('status')
      .access('body')
    
    
    // proto.url = proto.request.url
    delegate(proto, 'request')
      .access('url')
      .getter('header')
    

      

    context.js代理了requestresponsectx.body指向ctx.response.body。但是此时ctx.response ctx.request还没注入! 

    可能会有疑问,为什么response.jsrequest.js使用get set代理,而context.js使用delegate代理? 原因主要是: setget方法里面还可以加入一些自己的逻辑处理。而delegate就比较纯粹了,只代理属性。

    {
      get length() {
        // 自己的逻辑
        const len = this.get('Content-Length');
        if (len == '') return;
        return ~~len;
      },
    }
    
    // 仅仅代理属性
    delegate(proto, 'response')
      .access('length')
    

      

    因此context.js比较适合使用delegate,仅仅是代理requestresponse的属性和方法。 

    真正注入原生对象,是在application.js里的createContext方法中注入的!!!

    const http = require('http');
    const context = require('./context');
    const request = require('./request');
    const response = require('./response');
    
    class Application {
      constructor() {
        this.callbackFn = null;
        // 每个Kao实例的context request respones
        this.context = Object.create(context);
        this.request = Object.create(request);
        this.response = Object.create(response);
      }
    
      use(fn) {
        this.callbackFn = fn;
      }
    
      callback() {
        const handleRequest = (req, res) => {
          const ctx = this.createContext(req, res);
          return this.handleRequest(ctx)
        };
    
        return handleRequest;
      }
    
      handleRequest(ctx) {
        const handleResponse = () => respond(ctx);
        // callbackFn是个async函数,最后返回promise对象
        return this.callbackFn(ctx).then(handleResponse);
      }
    
      createContext(req, res) {
        // 针对每个请求,都要创建ctx对象
        // 每个请求的ctx request response
        // ctx代理原生的req res就是在这里代理的
        let ctx = Object.create(this.context);
        ctx.request = Object.create(this.request);
        ctx.response = Object.create(this.response);
        ctx.req = ctx.request.req = req;
        ctx.res = ctx.response.res = res;
        ctx.app = ctx.request.app = ctx.response.app = this;
        return ctx;
      }
    
      listen(...args) {
        const server = http.createServer(this.callback());
        return server.listen(...args);
      }
    }
    
    module.exports = Application;
    
    function respond(ctx) {
      // 根据ctx.body的类型,返回最后的数据
      /* 可能的类型,代码删减了部分判断
      1. string
      2. Buffer
      3. Stream
      4. Object
      */
      let content = ctx.body;
      if (typeof content === 'string') {
        ctx.res.end(content);
      }
      else if (typeof content === 'object') {
        ctx.res.end(JSON.stringify(content));
      }
    }
    

      

    代码中使用了Object.create的方法创建一个全新的对象,通过原型链继承原来的属性。这样可以有效的防止污染原来的对象。 

    createContext在每次http请求时都会调用,每次调用都新生成一个ctx对象,并且代理了这次http请求的原生的对象。

    respond才是最后返回http响应的方法。根据执行完所有中间件后ctx.body的类型,调用res.end结束此次http请求。

    现在我们再来测试一下:
    kao/index.js

     

    const Kao = require('./application');
    const app = new Kao();
    
    // 使用ctx修改状态码和响应内容
    app.use(async (ctx) => {
      ctx.status = 200;
      ctx.body = {
        code: 1,
        message: 'ok',
        url: ctx.url
      };
    });
    
    app.listen(3001, () => {
      console.log('server start at 3001');
    });
    

      

    中间件机制 

    参考代码: step-3

    const greeting = (firstName, lastName) => firstName + ' ' + lastName
    const toUpper = str => str.toUpperCase()
    
    const fn = compose([toUpper, greeting]);
    
    const result = fn('jack', 'smith');
    
    console.log(result);
    

      

    函数式编程有个compose的概念。比如把greetingtoUpper组合成一个复合函数。调用这个复合函数,会先调用greeting,然后把返回值传给toUpper继续执行。 

    实现方式:

    // 命令式编程(面向过程)
    function compose(fns) {
      let length = fns.length;
      let count = length - 1;
      let result = null;
    
      return function fn1(...args) {
        result = fns[count].apply(null, args);
        if (count <= 0) {
          return result
        }
    
        count--;
        return fn1(result);
      }
    }
    
    // 声明式编程(函数式)
    function compose(funcs) {
      return funcs.reduce((a, b) => (...args) => a(b(...args)))
    }
    

      

    Koa的中间件机制类似上面的compose,同样是把多个函数包装成一个,但是koa的中间件类似洋葱模型,也就是从A中间件执行到B中间件,B中间件执行完成以后,仍然可以再次回到A中间件。 

    Koa使用了koa-compose实现了中间件机制,源码非常精简,但是有点难懂。建议先了解下递归

    function compose (middleware) {
      if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
      for (const fn of middleware) {
        if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
      }
    
      /**
       * @param {Object} context
       * @return {Promise}
       * @api public
       */
    
      return function (context, next) {
        // last called middleware #
        let index = -1
        return dispatch(0)
        function dispatch (i) {
          // 一个中间件里多次调用next
          if (i <= index) return Promise.reject(new Error('next() called multiple times'))
          index = i
          // fn就是当前的中间件
          let fn = middleware[i]
          if (i === middleware.length) fn = next // 最后一个中间件如果也next时进入(一般最后一个中间件是直接操作ctx.body,并不需要next了)
          if (!fn) return Promise.resolve() // 没有中间件,直接返回成功
          try {
            
            /* 
              * 使用了bind函数返回新的函数,类似下面的代码
              return Promise.resolve(fn(context, function next () {
                return dispatch(i + 1)
              }))
            */
            // dispatch.bind(null, i + 1)就是中间件里的next参数,调用它就可以进入下一个中间件
    
            // fn如果返回的是Promise对象,Promise.resolve直接把这个对象返回
            // fn如果返回的是普通对象,Promise.resovle把它Promise化
            return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
          } catch (err) {
            // 中间件是async的函数,报错不会走这里,直接在fnMiddleware的catch中捕获
            // 捕获中间件是普通函数时的报错,Promise化,这样才能走到fnMiddleware的catch方法
            return Promise.reject(err)
          }
        }
      }
    }
    

      

    const context = {};
    
    const sleep = (time) => new Promise(resolve => setTimeout(resolve, time));
    
    const test1 = async (context, next) => {
      console.log('1-start');
      context.age = 11;
      await next();
      console.log('1-end');
    };
    
    const test2 = async (context, next) => {
      console.log('2-start');
      context.name = 'deepred';
      await sleep(2000);
      console.log('2-end');
    };
    
    const fn = compose([test1, test2]);
    
    fn(context).then(() => {
      console.log('end');
      console.log(context);
    });
    

      

    递归调用栈的执行情况: 

    弄懂了中间件机制,我们应该可以回答之前的问题:

    next到底是啥?洋葱模型是怎么实现的?

    next就是一个包裹了dispatch的函数

    在第n个中间件中执行next,就是执行dispatch(n+1),也就是进入第n+1个中间件

    因为dispatch返回的都是Promise,所以在第n个中间件await next(); 进入第n+1个中间件。当第n+1个中间件执行完成后,可以返回第n个中间件

    如果在某个中间件中不再调用next,那么它之后的所有中间件都不会再调用了

    修改kao/application.js

    class Application {
      constructor() {
        this.middleware = []; // 存储中间件
        this.context = Object.create(context);
        this.request = Object.create(request);
        this.response = Object.create(response);
      }
    
      use(fn) {
        this.middleware.push(fn); // 存储中间件
      }
    
      compose (middleware) {
        if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
        for (const fn of middleware) {
          if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
        }
      
        /**
         * @param {Object} context
         * @return {Promise}
         * @api public
         */
      
        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)
            }
          }
        }
      }
      
    
      callback() {
        // 合成所有中间件
        const fn = this.compose(this.middleware);
    
        const handleRequest = (req, res) => {
          const ctx = this.createContext(req, res);
          return this.handleRequest(ctx, fn)
        };
    
        return handleRequest;
      }
    
      handleRequest(ctx, fnMiddleware) {
        const handleResponse = () => respond(ctx);
        // 执行中间件并把最后的结果交给respond
        return fnMiddleware(ctx).then(handleResponse);
      }
    
      createContext(req, res) {
        // 针对每个请求,都要创建ctx对象
        let ctx = Object.create(this.context);
        ctx.request = Object.create(this.request);
        ctx.response = Object.create(this.response);
        ctx.req = ctx.request.req = req;
        ctx.res = ctx.response.res = res;
        ctx.app = ctx.request.app = ctx.response.app = this;
        return ctx;
      }
    
      listen(...args) {
        const server = http.createServer(this.callback());
        return server.listen(...args);
      }
    }
    
    module.exports = Application;
    
    function respond(ctx) {
      let content = ctx.body;
      if (typeof content === 'string') {
        ctx.res.end(content);
      }
      else if (typeof content === 'object') {
        ctx.res.end(JSON.stringify(content));
      }
    }
    

      

    测试一下 

     

    const Kao = require('./application');
    const app = new Kao();
    
    app.use(async (ctx, next) => {
      console.log('1-start');
      await next();
      console.log('1-end');
    })
    
    app.use(async (ctx) => {
      console.log('2-start');
      ctx.body = 'hello tc';
      console.log('2-end');
    });
    
    app.listen(3001, () => {
      console.log('server start at 3001');
    });
    
    // 1-start 2-start 2-end 1-end
    

      

    错误处理机制 

    参考代码: step-4

    因为compose组合之后的函数返回的仍然是Promise对象,所以我们可以在catch捕获异常

    kao/application.js

    handleRequest(ctx, fnMiddleware) {
      const handleResponse = () => respond(ctx);
      const onerror = err => ctx.onerror(err);
      // catch捕获,触发ctx的onerror方法
      return fnMiddleware(ctx).then(handleResponse).catch(onerror);
    }
    

      

    kao/context.js 

    const proto = module.exports = {
      // context自身的方法
      onerror(err) {
        // 中间件报错捕获
        const { res } = this;
    
        if ('ENOENT' == err.code) {
          err.status = 404;
        } else {
          err.status = 500;
        }
        this.status = err.status;
        res.end(err.message || 'Internal error');
      }
    }
    

      

    const Kao = require('./application');
    const app = new Kao();
    
    app.use(async (ctx) => {
      // 报错可以捕获
      a.b.c = 1;
      ctx.body = 'hello tc';
    });
    
    app.listen(3001, () => {
      console.log('server start at 3001');
    });
    

      

    现在我们已经实现了中间件的错误异常捕获,但是我们还缺少框架层发生错误的捕获机制。我们可以让Application继承原生的Emitter,从而实现error监听 

    kao/application.js

    const Emitter = require('events');
    
    // 继承Emitter
    class Application extends Emitter {
      constructor() {
        // 调用super
        super();
        this.middleware = [];
        this.context = Object.create(context);
        this.request = Object.create(request);
        this.response = Object.create(response);
      }
    }
    

      

    kao/context.js 

    const proto = module.exports = {
      onerror(err) {
        const { res } = this;
    
        if ('ENOENT' == err.code) {
          err.status = 404;
        } else {
          err.status = 500;
        }
    
        this.status = err.status;
    
        // 触发error事件
        this.app.emit('error', err, this);
    
        res.end(err.message || 'Internal error');
      }
    }
    

      

    const Kao = require('./application');
    const app = new Kao();
    
    app.use(async (ctx) => {
      // 报错可以捕获
      a.b.c = 1;
      ctx.body = 'hello tc';
    });
    
    app.listen(3001, () => {
      console.log('server start at 3001');
    });
    
    // 监听error事件
    app.on('error', (err) => {
      console.log(err.stack);
    });
    

      

    至此我们可以了解到Koa异常捕获的两种方式: 

    • 中间件捕获(Promise catch)
    • 框架捕获(Emitter error)

     

    // 捕获全局异常的中间件
    app.use(async (ctx, next) => {
      try {
        await next()
      } catch (error) {
        return ctx.body = 'error'
      }
    }
    )
    

      

    总结

    // 事件监听
    app.on('error', err => {
      console.log('error happends: ', err.stack);
    });
    

      

    初始化阶段:
    Koa整个流程可以分成三步:

     

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

      

    new初始化一个实例,use搜集中间件到middleware数组,listen 合成中间件fnMiddleware,返回一个callback函数给http.createServer,开启服务器,等待http请求。 

    请求阶段:

    每次请求,createContext生成一个新的ctx,传给fnMiddleware,触发中间件的整个流程

    响应阶段:

    整个中间件完成后,调用respond方法,对请求做最后的处理,返回响应给客户端。

    参考下面的流程图:

  • 相关阅读:
    MMoE论文笔记
    李宏毅-ELMO, BERT, GPT
    vue 手写一个时间选择器
    this 的几种使用场景
    如何使用markdown编辑器编写文章
    sublime text 3 入门技巧与常见问题解决
    Flex布局介绍
    在github上实现页面托管预览功能
    数据挖掘经典算法——最大期望算法
    数据挖掘经典算法——先验算法
  • 原文地址:https://www.cnblogs.com/ygunoil/p/14430771.html
Copyright © 2011-2022 走看看