Koa 与 Express 对比

Koa 与 Express 对比

Question List

  • koa原理,为什么要用koa ( express 和 koa 对比)
  • 使用过的koa中间件
  • koa中response.sendresponse.roundedresponse.json发生了什么事,浏览器为什么能识别到它是一个json结构或是html
  • koa-bodyparser怎么来解析request

Answer

一、Express 框架

Express 是一个轻量级的 Web Framework,自带Router、路由规则等,早期版本的 Express 还有bodyParser,后期剥离为独立模块作为中间件管理。其中间件模型是基于 callback回调 实现。

源码理解 app.use(middleware())、router.handle、next

中间件 middlewares 是较多Web框架的核心概念,可以根据不同的业务场景,集成到框架中,进而增强框架的服务能力,而框架也是需要提供一套机制来保证中间件有序的执行。

在 Express 中,我们是通过 app.use(middleware()) 的方式注册中间件,见using-middleware文档。use的顺序和规则express都做了控制。我们可以看一下源码进行分析。

express.js

Express 服务实例将 Node.js 的 reqres 对象传递给 app.handle 函数,使得handle内部具有reqres 对象的控制权。handle函数还有一个叫 next 的参数, next 在中间件控制权起到了十分重要的作用。

代码: https://github.com/expressjs/express/blob/dc538f6e810bd462c98ee7e6aae24c64d4b1da93/lib/express.js#L37

application.js

app.handle中,如果是路由的情况,还会将控制权转给router.handle,并传入res、req、callback,app.use 方法作为 路由的 Router#use() 代理方法添加中间件到路由。

代码: https://github.com/expressjs/express/blob/dc538f6e810bd462c98ee7e6aae24c64d4b1da93/lib/application.js#L158

router/index.js

Router#use() 中使用了layer来存放中间件,类似一个等待执行的中间件堆叠层。

router.handle 方法三个参数为 res、req、out,第三个参数变化了名称为out,意思可以理解为这是要原路返回出去的。

https://github.com/expressjs/express/blob/dc538f6e810bd462c98ee7e6aae24c64d4b1da93/lib/router/index.js#L136

这里关键部分在于内部函数 next,next会去查找匹配layer对叠层,如果匹配到,将会通过proto.process_params 来处理,将参数传递给layer层并执行,最后 layer.handle_request 执行的就是路由的handle

function next(err) {
    var layerError = err === 'route'
      ? null
      : err;
   ……
 // find next matching layer
    var layer;
    var match;
    var route;

    while (match !== true && idx < stack.length) {
      layer = stack[idx++];
      match = matchLayer(layer, path);
      route = layer.route;
      ……
    }
}

整个过程中,next 起到了关键作用——所有的中间件都要执行next,从而把当前的控制权以回调的方式往下面传递。

Express中response.sendresponse.json发生了什么事,浏览器为什么能识别到它是一个json结构或是html

response.json 本质上也是调用 response.send 方法,所以只需要分析一下response.send 的源码即可。

res.send 中通过判断chunk (body) 的类型,以及Content-Type 的值,来动态设置 Content-Type类型,使得浏览器知道响应的内容是什么类型数据。Express请求响应Content-Type类型常见有:

res.type('.html');
res.type('html');
res.type('json');
res.type('application/json');
res.type('png');

Express中间件 body-parser 如何解析request

从源码可以看到,body-parser 通过根据请求报文主体的压缩格式Content-Encoding 类型,将获取到请求的内容流进行解析。主要做了以下几点的实现:

  • 处理不同类型的请求体,如:textjsonurlencoded,对应主体的格式不同
  • 处理不同的编码:utf8gbk 等;
  • 处理不同的压缩类型:gzipdeflateidentity
  • 其他边界、异常的处理

简单使用,在Express中,通过设置请求为json格式

app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: true }))

二、Koa 框架

Koa 是一个新的 web 框架,由 Express 幕后的原班人马打造, 致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石。 通过利用 async 函数,Koa 帮你丢弃回调函数,并有力地增强错误处理。 Koa 并没有捆绑任何中间件, 而是提供了一套优雅的方法,帮助您快速而愉快地编写服务端应用程序。

Koa1.0 是基于 co 实现,通过 Generator/yield 来控制异步(详细了解co模块tj说:co是async/await的一块垫脚石)。随后 Koa2.0 改用 ES7 中的 async/await 来配合 Promise 实现异步控制。

Context

上下文对象ctx是由 createContext 创建的。主要把一些属性和变量挂载到 context 上,以及requestresponse。对于将 ctx 添加到整个应用程序中使用的属性或方法非常有用,这可能会更加有效(不需要中间件)和/或 更简单(更少的require()),而更多依赖于 ctx ,这可以被认为是一种反模式。

洋葱圈模型 & next()

当一个中间件调用 next(),则该函数暂停并将控制传递给定义的下一个中间件。当在下游没有更多的中间件执行后,堆栈展开并且每个中间件恢复执行其上游行为。

Koa 中间件构成实现模块是koa-compose (application源码构成中间件),是一个洋葱圈模型。

compose模块的源码也只有几十行:

/**
 * Compose `middleware` returning
 * a fully valid middleware comprised
 * of all those which are passed.
 *
 * @param {Array} middleware
 * @return {Function}
 * @api public
 */

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) {
      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)
      }
    }
  }
}

从源码可以看出,compose 对中间件进行了递归的操作,最终形成了一个中间件自执行链(只要第一个中间件执行了,随后的中间件都会依次被执行),这与koa1.0 版本基于co 实现一个目的,koa1.0 利用Thunk函数对 generator yield 异步操作封装成达到自执行目的。Koa2 之后,就改用 async/await 配合 promise 来实现了,上边代码就是中间件自执行操作的核心。

每个中间件都被封装成了一个 Promise对象。(这也是可以猜到的,因为 await 配合 Promise 才是最佳的。)

如下例子:

const Koa = require('koa');
const app = new Koa();


app.use(async function m1(ctx, next) {
    console.log('m1');
    await next(); // 暂停进入下一个中间件
    console.log('m1 end');
    const rt = ctx.response.get('X-Response-Time');
    console.log(`${ctx.method} ${ctx.url} - ${rt}`);
});


app.use(async function m2(ctx, next) {
    const start = Date.now();
    console.log('m2');
    await next(); // 暂停进入下一个中间件
    console.log('m2 end');
    const ms = Date.now() - start;
    ctx.set('X-Response-Time', `${ms}ms`);
});

app.use(async function m3(ctx, next) {
    console.log('m3');
    ctx.body = 'Hello World!';
});

app.listen(3000);

输出结果:


// 请求开始
m1
// m1中await next()进入暂停,进入下一个中间件m2
m2
//  m2中await next()进入暂停,进入下一个中间件m3
m3
//  洋葱模型,逆向回去,先m2的
m2 end
//  洋葱模型,逆向回去,m2执行完毕后进行上游m1的
m1 end
GET / - 2ms
// 响应结束

异常处理

Koa 还提供了异常处理的解决方式,统一的异常处理源码见ctx.onerror,我们可以使用 app.on('error',()=>{}) 来统一错误处理。


参考资料 https://www.zhihu.com/question/38879363 https://www.imooc.com/article/22994 https://www.cnblogs.com/chyingp/p/nodejs-learning-express-body-parser.html https://juejin.im/post/5a62bab4f265da3e58596f40


本人自动发布于:https://github.com/giscafer/blog/issues/23

相关文章