node.js express和koa
baymax 人气:0一、前言
大家可能都知道koa是express核心原班人马写的,那么他们为什么要在express后再造一个koa的轮子呢? 今天就给大家带来一些分析。希望能够起到一个抛砖引玉的作用。
其实,这个题目也可以这么问, express有什么缺点? koa解决了一些express的什么问题? 这也在一些面试题中会这么问。所以,为了实现自己的理想(money), 志同道合的同志们可以随我分析一下了。
我想先从express的一个非常重要的特征开始说起,那就是 中间件
。 中间件贯穿了express的始终,我们在express中比较常用到应用级的中间件,比如:
const app = require('express')(); app.use((req, res, next) => { // 做一些事情。。。 next(); })
再比如我们更常用到的路由级中间件。 我为什么要叫它是路由级的呢? 因为它的内部也同样维护着一个next
app.get('/', (req, res, next) => { res.send('something content'); })
这里中间件我不详细展开。 后面有我对中间件的详细解析,欢迎大家围观。
那么我们可以看到,其中会有个关键的next
, 它在express内部做的是从栈中获取下一个中间件的关键。
那么重点来了, 我们开始研究express这里的实现会隐藏什么问题。
二、中间件问题解析
通过一个例子来看:
const Express = require('express'); const app = new Express(); const sleep = () => new Promise(resolve => setTimeout(function(){resolve(1)}, 2000)); const port = 8210; function f1(req, res, next) { console.log('this is function f1....'); next(); console.log('f1 fn executed done'); } function f2(req, res, next) { console.log('this is function f2....'); next(); console.log('f2 fn executed done'); } async function f3(req, res) { console.log('f3 send to client'); res.send('Send To Client Done'); } app.use(f1); app.use(f2); app.use(f3); app.get('/', f3) app.listen(port, () => console.log(`Example app listening on port ${port}!`))
理想下的返回,和真正的返回,目前是没有问题的。
this is function f1.... this is function f2.... f3 send to client f1 fn executed done f2 fn executed done
好的,那么再继续下一个例子。 在下一个例子中,其它都是没有变化的,只有一个地方:
const sleep = () => new Promise(resolve => setTimeout(function(){resolve()}, 1000)) async function f3(req, res) { await sleep(); console.log('f3 send to client'); res.send('Send To Client Done'); }
这时你认为的返回值顺序是什么样的呢?
可能会认为跟上面的没有变化,因为我们增加await了,照道理应该等待await执行完了,再去执行下面的代码。 其实结果并不是。
返回的结果是:
this is function f1....
this is function f2....
f1 fn executed done
f2 fn executed done
f3 send to client
发生了什么??
大家可能有点吃惊。但是,如果深入到express的源码中去一探究竟,问题原因也就显而易见了。
具体源码我在这一篇中就不详细分析了,直接说出结论:
因为express中的中间件调用不是Promise
所以就算我们加了async await 也不管用。
那么koa
中是怎么使用的呢?
const Koa = require('koa'); const app = new Koa(); const sleep = () => new Promise(resolve => setTimeout(function(){resolve()}, 1000)) app.use(async (ctx, next) => { console.log('middleware 1 start'); await next(); console.log('middleware 1 end'); }); app.use(async (ctx, next) => { await sleep(); console.log('middleware 2 start'); await next(); console.log('middleware 2 end'); }); app.use(async (ctx, next) => { console.log('middleware 3 start') ctx.body = 'test middleware executed'; })
不出所料, 实现的顺序是:
middleware 1 start
middleware 2 start
middleware 3 start
middleware 2 end
middleware 1 end
原因是: koa 内部使用了Promise
,所以能够控制顺序的执行。
综合上面的例子,我们知道了express中中间件使用的时候,如果不清楚原理,是容易踩坑的。 而koa通过使用async 和 await next()
实现洋葱模型,即:通过next,到下一个中间件,只要下面的中间件执行完成后,才一层层的再执行上面的中间件,直到全部完成。
三、错误逻辑捕获
3.1 express的错误捕获逻辑
同样,先看express在错误逻辑的捕获
上有什么特点:
app.use((req, res, next) => { // c 没有定义 const a = c; }); // 错误处理中间件 app.use((err, req, res, next) => { if(error) { console.log(err.message); } next() }) process.on("uncaughtException", (err) => { console.log("uncaughtException message is::", err); })
再看一个异步的处理:
app.use((req, res, next) => { // c 没有定义 try { setTimeout(() => { const a = c; next() }, 0) } catch(e) { console.log('异步错误,能catch到么??') } }); app.use((err, req, res, next) => { if(error) { console.log('这里会执行么??', err.message); } next() }) process.on("uncaughtException", (err) => { console.log("uncaughtException message is::", err); })
可以先猜一下同步和异步的会不会有所区别?
答案是: 有很大的区别!!
具体分开来看:
- 同步的时候, 不会触发
uncaughtException
, 而进入了错误处理的中间件。 - 异步的时候,
不会
触发错误处理中间件,而会
触发uncaughtException
这中间发生了什么?
3.2 同步逻辑错误获取的底层逻辑
逻辑是: express内部对同步发生的错误进行了拦截,所以,不会传到负责兜底的node事件 uncaughtException
,如果发生了错误,则直接绕过其它中间件,进入错误处理中间件。 那么,这里会有一个很容易被忽略的点, 那就是,即使没有错误处理中间件做兜底,也不会进入node的 uncaughtException
, 这时, 会直接报 500
错误。
3.3 异步逻辑错误获取的底层逻辑
还是因为express的实现并没有把Promise考虑进去, 它的中间件执行是同步顺序
执行的。 所以如果有异步的,那么错误处理中间件实际是兜不住的,所以,express对这种中间件中的异步处理错误无能为力。
从上面的异步触发例子来看, 除了错误处理中间件没有触发,我们当中的try catch也没有触发
。这是一个大家可能都会踩到的坑。 这里其实是与javascript的运行机制相关了。具体原因见本篇 JavaScript异步队列进行try catch时的问题解决
所以要想去catch 当前的错误,那么就需要用 async await
app.use(async (req, res, next) => { try { await (() => new Promise((resolve, reject) => { http.get('http://www.example.com/testapi/123', res => { reject('假设错误了'); }).on('error', (e) => { throw new Error(e); }) }))(); } catch(e) { console.log('异步错误,能catch到么??') } });
这样,我们的catch不仅可以获取到, uncaughtException也可以获取到。
3.4 koa的错误获取逻辑
总体上是跟express差不多,因为js的底层处理还是一致的。但还是使用上有所差异。
上面也提过洋葱模型,特点是最开始的中间件,在最后才执行完毕,所以,在koa上,可以把错误处理中间件放到中间件逻辑最前面。
const http = require('http'); const Koa = require('koa'); const app = new Koa(); app.use(async (ctx, next)=>{ try { await next(); } catch (error) { // 响应用户 ctx.status = 500; ctx.body = '进入默认错误中间件'; // ctx.app.emit('error', error); // 触发应用层级错误事件 } }); app.use(async (ctx, next) => { await (() => new Promise((resolve, reject) => { http.get('http://www.example.com/testapi/123', res => { reject('假设错误了'); }).on('error', (e) => { throw new Error(e); }) }))(); await next(); })
上面的代码, reject出的错误信息,会被最上面的错误处理中间件捕获。总结来说,js的底层机制是一样的, 只是使用方法和细节点上不一样,大家在用的时候注意一下,
加载全部内容