一、前言
大家可能都知道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的底层机制是一样的, 只是使用方法和细节点上不一样,大家在用的时候注意一下,
到此这篇关于node.js express和koa中间件机制和错误处理机制的文章就介绍到这了,更多相关node.js express和koa内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!