从源码分析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按顺序执行。

posted @ 2020-05-27 14:51  浮云随笔  阅读(361)  评论(0编辑  收藏  举报