关于node.js:express和koa的区别

3次阅读

共计 8046 个字符,预计需要花费 21 分钟才能阅读完成。

express 评论:繁琐简单!虽有精妙中间件设计,其余设计过于简单 层层回调和递归
koa2 评论:精简彪悍!新 js 规范(async..await) 很好实现中间件

1、express 用法和 koa 用法简略展现

如果你应用 express.js 启动一个简略的服务器,那么根本写法应该是这样:

const express = require('express')

const app = express()
const router = express.Router()

app.use(async (req, res, next) => {console.log('I am the first middleware')
  next()
  console.log('first middleware end calling')
})
app.use((req, res, next) => {console.log('I am the second middleware')
  next()
  console.log('second middleware end calling')
})

router.get('/api/test1', async(req, res, next) => {console.log('I am the router middleware => /api/test1')
  res.status(200).send('hello')
})

router.get('/api/testerror', (req, res, next) => {console.log('I am the router middleware => /api/testerror')
  throw new Error('I am error.')
})

app.use('/', router)

app.use(async(err, req, res, next) => {if (err) {console.log('last middleware catch error', err)
    res.status(500).send('server Error')
    return
  }
  console.log('I am the last middleware')
  next()
  console.log('last middleware end calling')
})

app.listen(3000)
console.log('server listening at port 3000')

换算成等价的 koa2,那么用法是这样的:

const koa = require('koa')
const Router = require('koa-router')

const app = new koa()
const router = Router()

app.use(async(ctx, next) => {console.log('I am the first middleware')
  await next()
  console.log('first middleware end calling')
})

app.use(async (ctx, next) => {console.log('I am the second middleware')
  await next()
  console.log('second middleware end calling')
})

router.get('/api/test1', async(ctx, next) => {console.log('I am the router middleware => /api/test1')
  ctx.body = 'hello'
})

router.get('/api/testerror', async(ctx, next) => {throw new Error('I am error.')
})

app.use(router.routes())

app.listen(3000)
console.log('server listening at port 3000')

于是二者的应用区别通过表格展现如下:

koa(Router = require(‘koa-router’)) express(假如不应用 app.get 之类的办法)
初始化 const app = new koa() const app = express()
实例化路由 const router = Router() const router = express.Router()
app 级别的中间件 app.use app.use
路由级别的中间件 router.get router.get
路由中间件挂载 app.use(router.routes()) app.use(‘/’, router)
监听端口 app.listen(3000) app.listen(3000)

区别:koa 用的新规范,因二者外部实现机制不同,挂载路由中间件也有差别

重点:便是放在二者的中间件的实现上

2、express.js 中间件实现原理

demo 展现 express.js 的中间件在解决某些问题上的弱势

const express = require('express')

const app = express()

const sleep = (mseconds) => new Promise((resolve) => setTimeout(() => {console.log('sleep timeout...')
  resolve()}, mseconds))

app.use(async (req, res, next) => {console.log('I am the first middleware')
  const startTime = Date.now()
  console.log(`================ start ${req.method} ${req.url}`, {query: req.query, body: req.body});
  next()
  const cost = Date.now() - startTime
  console.log(`================ end ${req.method} ${req.url} ${res.statusCode} - ${cost} ms`)
})
app.use((req, res, next) => {console.log('I am the second middleware')
  next()
  console.log('second middleware end calling')
})

app.get('/api/test1', async(req, res, next) => {console.log('I am the router middleware => /api/test1')
  await sleep(2000)
  res.status(200).send('hello')
})

app.use(async(err, req, res, next) => {if (err) {console.log('last middleware catch error', err)
    res.status(500).send('server Error')
    return
  }
  console.log('I am the last middleware')
  await sleep(2000)
  next()
  console.log('last middleware end calling')
})

app.listen(3000)
console.log('server listening at port 3000')

该 demo 中当申请 /api/test1 的时候打印后果是什么呢?

I am the first middleware
================ start GET /api/test1
I am the second middleware
I am the router middleware => /api/test1
second middleware end calling
================ end GET /api/test1 200 - 3 ms
sleep timeout...

如果你分明这个打印后果的起因,想必对 express.js 的中间件实现有肯定的理解。

咱们先看看第一节 demo 的打印后果是:

I am the first middleware
I am the second middleware
I am the router middleware => /api/test1
second middleware end calling
first middleware end calling

这个打印合乎大家的冀望,然而为什么方才的 demo 打印的后果就不合乎冀望了呢?二者惟一的区别就是第二个 demo 加了异步解决。有了异步解决,整个过程就乱掉了。因为咱们冀望的执行流程是这样的:

I am the first middleware
================ start GET /api/test1
I am the second middleware
I am the router middleware => /api/test1
sleep timeout...
second middleware end calling
================ end GET /api/test1 200 - 3 ms

那么是什么导致这样的后果呢?咱们在接下去的剖析中能够失去答案。

2.1、express 挂载中间件的形式

要了解其实现:咱们得先晓得 express.js 到底有多少种形式能够挂载中间件进去?
挂载办法:(HTTP Method 指代那些 http 申请办法,诸如 Get/Post/Put 等等)

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

2.2、express 中间件初始化

express 代码中依赖于几个变量(实例):app、router、layer、route,这几个实例之间的关系决定了中间件初始化后造成一个数据模型,画了上面一张图片来展现:

看下面两张图,咱们抛出上面几个问题,搞懂问题便是搞懂了初始化。

  • 初始化模型图 Layer 实例为什么分两种?
  • 初始化模型图 Layer 实例中 route 字段什么时候会存在?
  • 初始化实例图中挂载的中间件为什么有 7 个?
  • 初始化实例图中圈 2 和圈 3 的 route 字段不一样,而且 name 也不一样,为什么?
  • 初始化实例图中的圈 4 里也有 Layer 实例,这个时候的 Layer 实例和下面的 Layer 实例不一样吗?

首先咱们先输入这样的一个概念:Layer 实例是 path 和 handle 相互映射的实体,每一个 Layer 便是一个中间件。

这样的话,咱们的中间件中就有可能嵌套中间件,那么看待这种情景,express 就在 Layer 中做手脚。咱们分两种状况挂载中间件:

  1. 应用 app.userouter.use 来挂载的

    • app.use通过一系列解决之后最终也是调用 router.use
  2. 应用 app.allapp.[Http Method]app.routerouter.allrouter.[Http Method]router.route 来挂载的

    • app.allapp.[Http Method]app.routerouter.allrouter.[Http Method]通过一系列解决之后最终也是调用 router.route

聚焦 router.userouter.route这两办法。

2.2.1、router.use

该办法的最外围一段代码是:

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);

  // 留神这个 route 字段设置为 undefined
  layer.route = undefined;

  this.stack.push(layer);
}

Layer 实例:初始化实例图圈 1 的所有 Layer 实例 ,自定义的中间件(共 5 个),还有两个零碎自带,看初始化实例图的 Layer 的名字别离是:queryexpressInit
二者的初始化是在 application.js 中的 lazyrouter 办法:

// application.js
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'))); // 最终调用的就是 router.use 办法
    this._router.use(middleware.init(this)); // 最终调用的就是 router.use 办法
  }
};

第三个问题:7 个中间件,2 个零碎自带、3 个 APP 级别的两头、2 个路由级别的中间件

2.2.2、router.route

app.allapp.[Http Method]app.routerouter.allrouter.[Http Method]
通过一系列解决之后最终也是调用 router.route
所以咱们在 demo 中的 express.js,应用了两次 app.get,其最初调用了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 这个
通过二者比照,咱们能够答复下面的好几个问题:

  • 初始化模型图 Layer 实例为什么分两种?
    因为调用形式的不同决定了 Layer 实例的不同
    第二种 Layer 实例是挂载在 route 实例之下的
  • 初始化模型图 Layer 实例中 route 字段什么时候会存在?
    应用 router.route 的时候就会存在
    最初一个问题,既然实例化 route 之后,route 有了本人的 Layer

那么它的初始化又是在哪里的?初始化外围代码:

// router/route.js/Route.prototype[method]
for (var i = 0; i < handles.length; i++) {var handle = handles[i];
    if (typeof handle !== 'function') {var type = toString.call(handle);
      var msg = 'Route.' + method + '() requires a callback function but got a' + type
      throw new Error(msg);
    }
    debug('%s %o', method, this.path)
    var layer = Layer('/', {}, handle);
    layer.method = method;

    this.methods[method] = true;
    this.stack.push(layer);
  }

能够看到新建的 route 实例,保护的是一个 path,对应多个 method 的 handle 的映射
每一个 method 对应的 handle 都是一个 layer,path 对立为/

至此,再回去看初始化模型图,置信大家能够有所明确了吧~

2.3、express 中间件的执行逻辑

整个中间件的执行逻辑无论是外层 Layer,还是 route 实例的 Layer,都是采纳递归调用模式,一个十分重要的函数 next() 流程图太简单,不看了:

咱们再把 express.js 的代码应用另外一种模式实现,这样你就能够齐全搞懂整个流程了。

为了简化,咱们把零碎挂载的两个默认中间件去掉,把路由中间件去掉一个,最终的成果是:

((req, res) => {console.log('I am the first middleware');
  ((req, res) => {console.log('I am the second middleware');
    (async(req, res) => {console.log('I am the router middleware => /api/test1');
      await sleep(2000)
      res.status(200).send('hello')
    })(req, res)
    console.log('second middleware end calling');
  })(req, res)
  console.log('first middleware end calling')
})(req, res)

因为没有对 await 或者 promise 的任何解决,所以当中间件存在异步函数的时候,因为整个 next 的设计起因,并不会期待这个异步函数 resolve, 于是咱们就看到了 sleep 函数的打印被放在了最初面,并且第一个中间件想要记录的申请工夫也变得不再精确了~

3、koa2 中间件

koa2 中间件的主解决逻辑放在了 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) {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)
      }
    }
  }
}

每个中间件调用的 next()其实就是这个:

dispatch.bind(null, i + 1)

还是利用闭包和递归的性质,一个个执行,并且每次执行都是返回 promise,所以最初失去的打印后果也是如咱们所愿。那么路由的中间件是否调用就不是 koa2 管的,这个工作就交给了koa-router,这样 koa2 才能够放弃精简彪悍的格调。

再贴出 koa 中间件的执行流程吧:

正文完
 0