之前一直使用Express搭建web服务端,从3.x到4.x,最近开始接触KOA2,突然有种如沐春风的感觉,除了相比Express更加简洁的语法外,他的异常处理机制也是让我眼前一亮。
之前用Express的时候异常的捕获常用到的方法有:
综上所述Express在异常的捕获和处理上是比较繁琐的,需要结合上面的三种方法,才能写出健壮性比较好的代码。但KOA的异常处理就相对简单明了多了。
异常捕获
来看一下KOA是怎样捕获异常的:
const http = require('http');
const https = require('https');
const Koa = require('koa');
const app = new Koa();
app.use((ctx)=>{
str="hello koa2";//沒有声明变量
ctx.body=str;
})
app.on("error",(err,ctx)=>{//捕获异常记录错误日志
console.log(new Date(),":",err);
});
http.createServer(app.callback()).listen(3000);
上面的代码运行后在浏览器访问返回的结果是“Internal Server error”;我们发现当错误发生的时候后端程序并没有死掉,只是抛出了异常,前端也同时接收到了错误反馈,是不是感觉比express简单多了?
其实不管是KOA还是Express其实异常都是发生在请求的处理过程中,对于KOA来说,异常发生在中间件的执行过程中,所以只要我们在中间件执行过程中将异常捕获并处理就OK了。
下面我们来看看中间件的执行过程,首先就是添加中间件use方法:
use(fn) {
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
if (isGeneratorFunction(fn)) {
deprecate('Support for generators will be removed in v3. ' +
'See the documentation for examples of how to convert old middleware ' +
'https://github.com/koajs/koa/blob/master/docs/migration.md');
fn = convert(fn);
}
debug('use %s', fn._name || fn.name || '-');
this.middleware.push(fn);
return this;
}
fn可以是三种类型的函数,普通函数,generator函数,还有async函数。最后generator会被转成async函数,关于async和generator可以看看]async 函数的含义和用法这篇文章。所以最终中间件数组只会有普通函数和async函数。
下面我们在来看看中间件对象middleware在的使用:
const fn = compose(this.middleware);
koa-compose
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, function next () {
return dispatch(i + 1)
}))
} catch (err) {
return Promise.reject(err)
}
}
}
}
compose的作用就是将所有的中间件生成一个中间件自执行链,有点类似co模块。这样我们只需要执行第一个中间件,后面的中间件就会依次执行。可以发现每个中间件都被被封装成了一个Promise,其实这里我刚刚开始看的时候还是有一点蒙圈,主要还是对async和Promise.resolve和new Promise这之间的区别傻傻分不清。Promise.resolve和其实就是返回一个成功状态的Promise对象,相当于:
new Promise((resolve)=>resolve());
同理Promise.reject就相当于
new Promise((resolve,reject)=>reject())
具体KOA2中中间件的自执行实现我后面会专门分析,我还是接着分析在执行过程中的异常捕获和处理。
前面我们说了middleware里面只有两类函数普通函数和async函数,所以,我们在中间件中就考虑这两类函数在执行时的异常捕获。
首先是普通函数,普通函数的正常异常可以直接被try catch捕获,然后返回一个失败状态的Promise,但是但是如果在普通函數中写异步代码,在异步代码中发生的异常时没法捕获的,会直接导致服务断掉,比如:
app.get("/",(ctx,next)=>{setTimeout(()=> new Error("this is an error"),1000)})
在KOA2 中是不推荐使用这种异步程序的,异步程序全部可以使用asyn函数来将异步转换成同步代码块。
第二种就是async函数了:
async function getFile(){
console.info(`start time[${new Date()}]`);
try{
await readFile();
}catch(e){
console.log("occur error:",e);
}
console.info(`endtime[${new Date()}]`);
}
function readFile(){
return new Promise((resolve,reject)=>{
setTimeout(()=>{resolve("ok")},1000)
})
}
getFile();
其实async 函数中的await是在等待一个resolved状态的Promise对象,简单来说await就是一个then的语法糖,并沒有catch,所以不会捕获异常, 那就需要使用try/catch来捕获异常,并进行相应的逻辑处理。
在説以我們在dispatch中看見了這一段代碼
try {
return Promise.resolve(fn(context, function next () {
return dispatch(i + 1)
}))
} catch (err) {
return Promise.reject(err)
}
通过这一段代码就可以在中间件执行链顺利执行结束时返回一个resolved状态的Promise,在发生异常的时候返回一个rejected状态的Promise,在application.js中通过下面这段代码处理这两种状态
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
通过最终通过Promise对象的catch来捕获到异常。
异常处理
当异常捕获是有两种处理方式,一种就是响应错误请求,而就是触发注册注册全局错误事件,比如记录错误日志…,先来看看我们前面提到的发生异常的时候前端收到的“Internal Server error”是怎么来的。
catch后交给了ctx.onerror处理,ctx.onerror在context.js中有定义:
context.js
onerror(err) {
//判断err是否存在
if (null == err) return;
if (!(err instanceof Error)) err = new Error(util.format('non-error thrown: %j', err));
let headerSent = false;
if (this.headerSent || !this.writable) {
headerSent = err.headerSent = true;
}
//通过Node的事件机制触发全局的error事件
this.app.emit('error', err, this);
if (headerSent) {
return;
}
const { res } = this;
// first unset all headers
if (typeof res.getHeaderNames === 'function') {
res.getHeaderNames().forEach(name => res.removeHeader(name));
} else {
res._headers = {}; // Node < 7.7
}
// then set those specified
this.set(err.headers);
// 设置响应类型text/plain
this.type = 'text';
// ENOENT support
if ('ENOENT' == err.code) err.status = 404;
// default to 500
if ('number' != typeof err.status || !statuses[err.status]) err.status = 500;
// 返回请求错误
const code = statuses[err.status];
const msg = err.expose ? err.message : code;
this.status = err.status;
this.length = Buffer.byteLength(msg);
this.res.end(msg);
}
};
通过这样法,当异常发生时程序就会主动的抛出异常,同时发生错误的请求响应,不需要我们在去手动的返回错误信息。
然后就是触发全局的自定义异常处理,在上面这段代码中出现了 this.app.emit(‘error’, err, this);这里触发了全局的error事件,我们再看看在哪里注册的事件,在application中出现了
callback() {
const fn = compose(this.middleware);
if (!this.listenerCount('error')) this.on('error', this.onerror);
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};
return handleRequest;
}
this是继承了node的事件对象的,所以可以通过on来注册全局事件,服务在启动的时候会默认注册一个error全局事件,我们知道node的事件机制可以同一个事件多次注册的,它维护的是这个事件的一个事件数组,所以我们可以自定error事件。这样我们在最前面写的
app.on("error",(err,ctx)=>{//捕获异常记录错误日志
console.log(new Date(),":",err);
});
也就能在异常发生时执行了。
总的来說KOA异常捕获和处理的核心就是try catch和nodejs的事件机制。什么?try catch,沒錯,就是他,前面不是說他不能捕获异步异常吗?他是不能捕获异步异常,但是KOA里面使用了async await他相当于把当前异步代码变成了同步处理,so,我们可以当做其实是在处理同步代码的异常。
大概就是这样了。不知道这么理解对不对,欢迎更正。