zoukankan      html  css  js  c++  java
  • 从源码分析express和koa分析区别

    在下面分别对express用法和koa用法简单进行简单展示

    Express

    import express from 'express';
    import routes  from '../Routes';
    import proxy from 'express-http-proxy';
    
    const app = express();
    app.use(express.static('public'));
    
    app.use('/api', proxy('http://localhost:4000', {
      proxyReqPathResolver: function(req) {
        return '/api'+req.url
      }
    }));
    
    app.use('/', routes)
     
    var server = app.listen(3001, function () {
      var host = server.address().address;
      var port = server.address().port;
      console.log("应用实例,访问地址为 http://%s:%s", host, port);
    })
    

    Koa

    import koa2 from 'koa2';
    import routes  from '../Routes';
    import proxy from 'koa2-proxy-middleware';
    const router = require('koa-router')()
    const proxyOptions = {
        target: 'http://localhost:4000', //后端服务器地址
        changeOrigin: true //处理跨域
    };
    const app = koa2();
    app.use(require('koa-static')(__dirname + '../public'))
    //api前缀的请求都走代理
    app.use(proxy('/api/*', proxyOptions));
    
    app.use(router.routes())
     
    var server = app.listen(3001, function () {
      var host = server.address().address;
      var port = server.address().port;
      console.log("应用实例,访问地址为 http://%s:%s", host, port);
    })
    

    目前可以挂载中间件进去的有:(HTTP Method指代那些http请求方法,诸如Get/Post/Put等等)

    • app.use
    • app.[HTTP Method]
    • app.all
    • app.param
    • router.all
    • router.use
    • router.param
    • router.[HTTP Method]

    express中间件

    express代码中依赖于几个变量(实例):app、router、layer、route

    Layer实例是path和handle互相映射的实体,每一个Layer便是一个中间件

    ++router.use++:使用app.use、router.use来挂载的,
    app.use经过一系列处理之后最终也是调用router.use的

    router.route:使用app.all、app.[Http Method]、app.route、router.all、router.[Http Method]、router.route来挂载的

    router.use 源码分析

    // mixin Router class functions
    setPrototypeOf(router, proto)
    
    proto.use = function use(fn) {
      var offset = 0;
      var path = '/';
    
      // default path to '/'
      // disambiguate router.use([fn])
      if (typeof fn !== 'function') {
        var arg = fn;
    
        while (Array.isArray(arg) && arg.length !== 0) {
          arg = arg[0];
        }
    
        // first arg is the path
        if (typeof arg !== 'function') {
          offset = 1;
          path = fn;
        }
      }
    
      var callbacks = flatten(slice.call(arguments, offset));
    
      if (callbacks.length === 0) {
        throw new TypeError('Router.use() requires a middleware function')
      }
    
      for (var i = 0; i < callbacks.length; i++) {
        var fn = callbacks[i];
    
        if (typeof fn !== 'function') {
          throw new TypeError('Router.use() requires a middleware function but got a ' + gettype(fn))
        }
    
        // add the middleware
        debug('use %o %s', path, fn.name || '<anonymous>')
    
        var layer = new Layer(path, {
          sensitive: this.caseSensitive,
          strict: false,
          end: false
        }, fn);
    
        layer.route = undefined;
    
        this.stack.push(layer);
      }
    
      return this;
    };
    

    从以上代码可以看出, router.use 就是初始化多个 layer 实例

    // setup router
    this.lazyrouter();
    

    在启动 router 的时候,调用了 this.lazyrouter() 方法

    // lazily adds the base router if it has not yet been added
    app.lazyrouter = function lazyrouter() {
      if (!this._router) {
        this._router = new Router({
          caseSensitive: this.enabled('case sensitive routing'),
          strict: this.enabled('strict routing')
        });
    
        this._router.use(query(this.get('query parser fn')));
        this._router.use(middleware.init(this));
      }
    };
    

    两个系统自带的,看初始化实例图的Layer的名字分别是:query 和 middleware.init, 这两个方法最终调用的也是router.use方法

    router.route

    proto.route = function route(path) {
      var route = new Route(path);
    
      var layer = new Layer(path, {
        sensitive: this.caseSensitive,
        strict: this.strict,
        end: true
      }, route.dispatch.bind(route));
    
      layer.route = route;
    
      this.stack.push(layer);
      return route;
    };
    
    

    从以上源码中可以看出, new Route 实例化了一个Route, layer.route = route,其他的跟 router.use 差不多 .源码中初始Layer,其中的回调是route.dispatch.bind(route)

    function Layer(path, options, fn) {
      if (!(this instanceof Layer)) {
        return new Layer(path, options, fn);
      }
    
      debug('new %o', path)
      var opts = options || {};
    
      this.handle = fn;
      this.name = fn.name || '<anonymous>';
      this.params = undefined;
      this.path = undefined;
      this.regexp = pathRegexp(path, this.keys = [], opts);
    
      // set fast path flags
      this.regexp.fast_star = path === '*'
      this.regexp.fast_slash = path === '/' && opts.end === false
    }
    
    

    我们如果使用箭头函数,不存在函数名,打印出来的 layer 的 name 是 anonymous

    Route.prototype.all = function all() {
      var handles = flatten(slice.call(arguments));
    
      for (var i = 0; i < handles.length; i++) {
        var handle = handles[i];
    
        if (typeof handle !== 'function') {
          var type = toString.call(handle);
          var msg = 'Route.all() requires a callback function but got a ' + type
          throw new TypeError(msg);
        }
    
        var layer = Layer('/', {}, handle);
        layer.method = undefined;
    
        this.methods._all = true;
        this.stack.push(layer);
      }
    
      return this;
    };
    

    新建的route实例,维护的是一个path,对应多个method的handle的映射。每一个method对应的handle都是一个layer,path统一为/

    ((req, res) => {
        ……
        (async(req, res) => {
        ……
        })(req, res)
    })(req, res)
    

    实际上 express 的表现形式大概如此

    koa2中间件

    当通过 require 去载入 koa 模块时,找到模块下的package.json中的:

     "main": "lib/application.js",
    

    最终指向

    module.exports = class Application extends Emitter {
    }
    

    挂载中间件

      /**
       * Use the given middleware `fn`.
       *
       * Old-style middleware will be converted.
       *
       * @param {Function} fn
       * @return {Application} self
       * @api public
       */
    
      use(fn) {
        if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
        if (isGeneratorFunction(fn)) {
          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);
        }
        debug('use %s', fn._name || fn.name || '-');
        this.middleware.push(fn);
        return this;
      }
    

    以上代码可以看出,首先检查是否是方法,其次判断是否是生成器函数,因为在Koa1.x版本中是通过Generator+Promise+Co实现的,因此将中间件定义成了Generator Function。但自从Koa v2版本起,它的异步控制方案就开始支持Async/Await,因此中间件也用普通函数就可以了

    convert:即koa-convert,作用是加入了一层函数嵌套,并使用Co自动执行原Generator函数

    最后一段代码的作用是把传入的函数,push到this.middleware属性的尾部,this.middleware是一个数组,用来存储中间件

    this.middleware = [];
    

    响应请求

      /**
       * Shorthand for:
       *
       *    http.createServer(app.callback()).listen(...)
       *
       * @param {Mixed} ...
       * @return {Server}
       * @api public
       */
    
      listen(...args) {
        debug('listen');
        const server = http.createServer(this.callback());
        return server.listen(...args);
      }
    

    通过 http.createServer 创建服务,this.callback() 方法中一般会res指定了响应头,响应体内容为node.js,用end结束,我们来继续看下源码。

      /**
       * Return a request handler callback
       * for node's native http server.
       *
       * @return {Function}
       * @api public
       */
    
      callback() {
        const fn = compose(this.middleware);
    
        if (!this.listenerCount('error')) this.on('error', this.onerror);
    
        const handleRequest = (req, res) => {
          const ctx = this.createContext(req, res);
          return this.handleRequest(ctx, fn);
        };
    
        return handleRequest;
      }
    

    其中ctx是网络请求上下文,我们继续看下this.handleRequest这个方法中有什么?

      /**
       * Handle request in callback.
       *
       * @api private
       */
    
      handleRequest(ctx, fnMiddleware) {
        const res = ctx.res;
        res.statusCode = 404;
        const onerror = err => ctx.onerror(err);
        const handleResponse = () => respond(ctx);
        onFinished(res, onerror);
        return fnMiddleware(ctx).then(handleResponse).catch(onerror);
      }
    
    

    这段代码中,主要执行了:

    • 错误处理:onerror函数
    • onFinished监听response执行完成,以用来做一些资源清理工作。
    • 执行传入的fnMiddleware
      -最终等待中间件执行完,最终执行handleResponse函数,开始组织响应,代码如下:
    /**
     * Response helper.
     */
    
    function respond(ctx) {
      // allow bypassing koa
      if (false === ctx.respond) return;
    
      if (!ctx.writable) return;
    
      const res = ctx.res;
      let body = ctx.body;
      const code = ctx.status;
    
      // ignore body
      if (statuses.empty[code]) {
        // strip headers
        ctx.body = null;
        return res.end();
      }
    
      if ('HEAD' == ctx.method) {
        if (!res.headersSent && isJSON(body)) {
          ctx.length = Buffer.byteLength(JSON.stringify(body));
        }
        return res.end();
      }
    
      // status body
      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
      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);
    }
    
    

    刚刚我们似乎漏下了一个主要的方法compose(this.middleware),它是如何来组织中间件的呢?

    const compose = require('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!')
      }
    
      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)
          }
        }
      }
    }
    
    

    在参数中context:是透传的网络对象上下文,next:目前是undefined,后面会说明,它是用来表示所有中间件走完之后,最后执行的一个函数。

    首先检查数组类型及数组里每个元素的类型,标识了一个变量index,等下讲dispatch函数的时候会看到它的作用 —— 用于标识「上一次执行到了哪个中间件」

      // 校验预期执行的中间件,其索引是否在已经执行的中间件之后
          if (i <= index) return Promise.reject(new Error('next() called multiple times'))
    

    执行过了的就不再执行

          let fn = middleware[i]
    

    取预期执行的中间件函数

      if (i === middleware.length) fn = next
    

    预期执行的中间件索引,已经超出了middleware边界,说明中间件已经全部执行完毕,开始准备执行之前传入的next

     if (!fn) return Promise.resolve()
    

    没有fn的话,直接返回一个已经reolved的Promise对象

     try {
            // 对中间件的执行结果包裹一层Promise.resolve
            return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
          } catch (err) {
            return Promise.reject(err)
          }
    
    

    通过递归,对中间件的执行结果包裹一层Promise.resolve,next相当于是上一个中间件对下一个中间件的执行权,调用 next(),执行下一个中间件。

    而Promise有个特性,如果Promise.resolve接受的参数,也是个Promise,那么外部的Promise会等待该内部的Promise变成resolved之后,才变成resolved,例如下面的这段代码:

    Promise.resolve(new Promise((resolve => {
    	setTimeout(() => { 
        console.log('A Resolved');
        resolve()
      }, 0);
    })))
      .then(() => { console.log('B Resolved')})
    
    // 先输出:A Resolved
    // 后输出:B Resolved
    
    

    总结

    koa2 源码中主要利用闭包和递归的性质,一个个执行,因为每次返回的时候promise.resolve()中的都是Promise对象,然后会去等待方法参数中的Promise执行完then然后再返回,因此如果中间键有await会先执行完异步,按顺序执行;而Express中不是通过promise.resolve()的方式因此无法保证中间件中的await按顺序执行。

  • 相关阅读:
    三点定位
    vue中使用UEditor编辑器 -- 2
    网络编程——socket编程
    异常处理
    面向对象进阶
    初识面向对象,面向对象之继承、多态和封装
    Python常用的模块
    函数递归,匿名、内置行数,模块和包,开发规范
    闭包、装饰器decorator、迭代器与生成器、面向过程编程、三元表达式、列表解析与生成器表达式
    Python基础学习——文件操作、函数
  • 原文地址:https://www.cnblogs.com/fuGuy/p/12973088.html
Copyright © 2011-2022 走看看