koa篇--koa2中异常处理机制

 之前一直使用Express搭建web服务端,从3.x到4.x,最近开始接触KOA2,突然有种如沐春风的感觉,除了相比Express更加简洁的语法外,他的异常处理机制也是让我眼前一亮。
之前用Express的时候异常的捕获常用到的方法有:

  • 在代码块中通过try catch来捕获异常,这种方法对于异步处理无法捕获。
  • 采用捕获进程中的uncaughtException事件,这种处理方式一方面发生请求错误的时候无法响应请求,這可能会导致服 务器内存溢出。
  • 采用domain的是方式,但是目前Nodejs官方已经放弃这个API了,具体原因不详。

综上所述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,我们可以当做其实是在处理同步代码的异常。
 大概就是这样了。不知道这么理解对不对,欢迎更正。

你可能感兴趣的:(nodejs,koa)