express和koa的区别

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和koa的区别】要理解其实现:我们得先知道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,这几个实例之间的关系决定了中间件初始化后形成一个数据模型,画了下面一张图片来展示:
express和koa的区别
文章图片

看上面两张图,我们抛出下面几个问题,搞懂问题便是搞懂了初始化。
  • 初始化模型图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 || '')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和koa的区别
文章图片

我们再把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中间件的执行流程吧:

    推荐阅读