1. 程式人生 > >koa篇--koa2中異常處理機制

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,我們可以當做其實是在處理同步程式碼的異常。
 大概就是這樣了。不知道這麼理解對不對,歡迎更正。