用了那么多年的express.js,终于有时间来深刻学习express,而后顺便再和koa2的实现方式对比一下。html
老实说,还没看express.js源码以前,一直以为express.js仍是很不错的,不管从api设计,仍是使用上都是能够的。可是此次阅读完express代码以后,我可能改变想法了。node
虽然express.js有着精妙的中间件设计,可是以当前js标准来讲,这种精妙的设计在如今能够说是太复杂。里面的层层回调和递归,不花必定的时间还真的很难读懂。而koa2的代码呢?简直能够用四个字评论:精简彪悍!仅仅几个文件,用上最新的js标准,就很好实现了中间件,代码读起来一目了然。git
老规矩,读懂这篇文章,咱们依然有一个简单的demo来演示: express-vs-koagithub
若是你使用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,那么用法是这样的:express
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')
复制代码
若是你还感兴趣原生nodejs启动服务器是怎么使用的,能够参考demo中的这个文件:node.jsapi
因而两者的使用区别经过表格展现以下:promise
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语法都是用的新标准。在挂载路由中间件上也有必定的差别性,这是由于两者内部实现机制的不一样。其余都是大同小异的了。bash
那么接下去,咱们的重点即是放在两者的中间件的实现上。服务器
咱们先来看一个demo,展现了express.js的中间件在处理某些问题上的弱势。demo代码以下:
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
复制代码
那么是什么致使这样的结果呢?咱们在接下去的分析中能够获得答案。
要理解其实现,咱们得先知道express.js到底有多少种方式能够挂载中间件进去?熟悉express.js的童鞋知道吗?知道的童鞋能够内心默默列举一下。
目前能够挂载中间件进去的有:(HTTP Method指代那些http请求方法,诸如Get/Post/Put等等)
express代码中依赖于几个变量(实例):app、router、layer、route,这几个实例之间的关系决定了中间件初始化后造成一个数据模型,画了下面一张图片来展现:
图中存在两块Layer实例,挂载的地方也不同,以express.js为例子,咱们经过调试找到更加形象的例子:
结合两者,咱们来聊聊express中间件初始化。为了方便,咱们把上图1叫作初始化模型图,上图2叫作初始化实例图
看上面两张图,咱们抛出下面几个问题,搞懂问题即是搞懂了初始化。
首先咱们先输出这样的一个概念:Layer实例是path和handle互相映射的实体,每个Layer即是一个中间件。
这样的话,咱们的中间件中就有可能嵌套中间件,那么对待这种情形,express就在Layer中作手脚。咱们分两种状况挂载中间件:
app.use
、router.use
来挂载的
app.use
通过一系列处理以后最终也是调用router.use
的app.all
、app.[Http Method]
、app.route
、router.all
、router.[Http Method]
、router.route
来挂载的
app.all
、app.[Http Method]
、app.route
、router.all
、router.[Http Method]
通过一系列处理以后最终也是调用router.route
的所以咱们把焦点聚焦在router.use
和router.route
这两个方法。
该方法的最核心一段代码是:
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实例,此时以express.js为例子,咱们看初始化实例图圈1的全部Layer实例,会发现除了咱们自定义的中间件(共5个),还有两个系统自带的,看初始化实例图的Layer的名字分别是:query
和expressInit
。两者的初始化是在[application.js]中的lazyrouter
方法:
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个路由级别的中间件
咱们说过app.all
、app.[Http Method]
、app.route
、router.all
、router.[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
这个。经过两者对比,咱们能够回答上面的好几个问题:
router.route
的时候就会存在anonymous
,可是圈3由于使用的router.route
,因此其统一的回调函数都是route.dispath
,所以其函数名字都统一是bound dispatch
,同时两者的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统一为/
。这样就轻松回答了最后一个问题了。
至此,再回去看初始化模型图,相信你们能够有所明白了吧~
整个中间件的执行逻辑不管是外层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
函数的打印被放在了最后面,而且第一个中间件想要记录的请求时间也变得再也不准确了~
可是有一点须要申明的是虽然打印变得奇怪,可是绝对不会影响整个请求,由于response是在咱们await以后,因此请求是否结束仍是取决于咱们是否调用了res.send这类函数
至此,但愿整个express中间件的执行流程你能够熟悉一二,更多细节建议看看源码,这种精妙的设计确实不是这篇文章可以说清楚的。本文只是想你在面试的过程当中能够作到有话要说~
接下去,咱们分析牛逼的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中间件的执行流程吧:
有了这篇文章,相信你不再怕面试官问你express和koa的区别了~