Koa從零搭建到Api實現—優雅的處理異常

前言

咱們在講解如何處理異常以前,須要先對Koa中間件機制進行了解。node

Koa中間件機制解析

koa 的請求處理是典型的洋蔥模型,下面是官方的配圖,而這一模型的組成部分就是 middlewareapp

koa 中間件的執行機制,就是一個洋蔥模型,每次請求進來,先執行前面的中間件,遇到 next,執行下一個中間件,以此重複,執行完全部中間件後,再從最後一箇中間件往前執行koa

例子:異步

app.use(async (ctx, next) => {
  console.log(1)
  await next()
  console.log(2)
})

app.use(async (ctx, next) => {
  console.log(3)
  await next()
  console.log(4)
})

app.use(async (ctx, next) => {
  console.log(5)
  await next()
  console.log(6)
})

app.use(async (ctx, next) => {
  console.log(7)
})

// 輸出結果是
1 3 5 7 6 4 2
思考 - 中間件在處理異常的過程當中扮演了什麼角色?

異常捕獲

常見拋出異常和錯誤類型async

  • 代碼語法不規範形成的JS報錯異常
  • 程序運行中發生的一些未知異常
  • HTTP錯誤
  • 自定義的業務邏輯錯誤

處理異常的方式

try catch

提及異常捕獲,咱們最早想到的確定是 try catch, 其在node下是如何實現?例子:學習

const func = async (ctx, next) => {
    try {
        await next()
    } catch () {
        ctx.body = { //返回異常 }
    }
}
app.use(func)
app.use(func1)
app.use(func2)

可是try catch 有個問題,它沒法處理異步代碼塊內出現的異常。能夠理解爲在執行catch時,異常還沒發生ui

try {
    asyncError()
} catch (e) {
    /*異常沒法被捕獲,致使進程退出*/
    console.log(e.message)
}

callback方式

fs.mkdir('/dir', function (e) {
    if (e) {
        /*處理異常*/
        console.log(e.message)
    } else {
        console.log('建立目錄成功')
    }
})

Promise 方式

new Promise((resolve, reject) => {
    syncError()
}).then(() => {
        //...
    }).catch((e) => {
        /*處理異常*/
        console.log(e.message)
    })

Promise一樣沒法處理異步代碼塊中拋出的異常this

throw方法

Koa提供了ctx.throw(400)的方式,讓咱們便捷的拋出http錯誤,可是咱們在拋出http錯誤的同時想返回額外的信息?該如何實現?url

ctx.throw(400, 'name required', { user: user });

若須要定義若干業務邏輯錯誤碼和說明,返回不一樣的code,在controller層面,你也許能夠這樣處理:spa

router.get('/', (ctx, next) => {
    if (checkToken(token)) {
        const code = ERROR_CODE.TOKEN_ERROR
        ctx.body = {
            code,
            msg: ERROR_MSG[code]
        }
        return
    }
    // do something
})

若是是在model層或者server層,要處理這樣的錯誤怎麼辦?

  1. 經過定義返回值來講明錯誤,在controller中判斷返回值再返回相應錯誤碼,好比:

    const somefunc = async (token) => {
      const res = await tokenExpire(token)
      if (res) {
        return false
      }
      // do something
    }
  2. 拋出Error,在controller中catch住異常,並對比err.message來返回相應錯誤碼,好比:

    const somefunc = async (token) => {
       const res = await tokenExpire(token)
       if (res) {
           throw Error(ERROR_MSG.TOKEN_ERROR)
       }
       // do something
     }

問題來了。

若是錯誤的類型,文言有不少種怎麼辦?
每次都須要進行if判斷,煩不煩?

process

process方式能夠捕獲任何異常(無論是同步代碼塊中的異常仍是異步代碼塊中的異常)

process.on('uncaughtException', function (e) {
    /*處理異常*/
    console.log(e.message)
});
asyncError()
syncError()

error事件的監聽方式

const Koa = require('koa')
const app = new Koa()
app.on('error', (err, next) => {
    console.error('server error',err)
})
const main = ctx => {
    ctx.throw(500)
}
app.use(main)
app.listen(3000)

中間件的處理方式

const Koa = require('koa')
const app = new Koa()
app.use( async (ctx, next) =>{
    await next().catch(error => {
        console.log(error)
    });
})
const main = ctx => {
    ctx.throw(500)
}
app.use(main)
app.listen(3000)

優雅的異常處理方案!

斷言庫-assert

首先咱們使用一個一個斷言庫!

  • assert

爲何要使用?參考throw方法中,咱們一般須要針對不一樣的業務邏輯場景進行返回錯誤。
好比: '用戶名不爲空','密碼不能爲空','起始日大於截止日','密碼輸入錯誤','用戶名不存在'...等等
若是採用throw方法,咱們須要定義不少code,msg來進行維護,好比:

ERROR_CODE:

ERROR_CODE = {
     SOME_CUSTOM_ERROR: 1001,
     EMPTY_PASSWORD: 1002
}

ERROR_MSG:

ERROR_MSG = {
  1001: 'some custom error msg',
  1002: '密碼不能爲空'
}

可是使用斷言庫以後,咱們不須要去寫以下代碼,不須要額外維護code,msg

if (!ctx.request.body.password) { throw(...) }
if (!ctx.request.body.name) { throw(...) }

只須要

assert.ok(data.password, 'password不能爲空')
 assert.ok(data.name, '用戶名不能爲空')

便可,返回以下

{
    "code": 500,
    "msg": "password不能爲空",
    "data": {},
    "success": false
}

定義HttpError和CustomError

利用koa中間件加上咱們自定義的繼承於Error構造器的方法即可以實現。

function CustomError (code, msg) {
  Error.call(this, '')
  this.code = code
  this.msg = msg || ERROR_MSG[code] || 'unknown error'
  this.getCodeMsg = function () {
    return {
      code: this.code,
      msg: this.msg
    }
  }
}
util.inherits(CustomError, Error)
function HttpError (code, msg) {
  if (Object.values(HTTP_CODE).indexOf(code) < 0) {
    throw Error('not an invalid http code')
  }
  CustomError.call(this, code, msg)
}
util.inherits(HttpError, CustomError)

拋出錯誤

router.get('/HttpError', (ctx, next) => {
  throw new HttpError(HTTP_CODE.FORBIDDEN)
})
const somefunc = async (token) => {
  const res = await tokenExpire(token)
  if (res) {
    throw new CustomError(CUSTOM_CODE.SOME_CUSTOM_ERROR)
  }
  // do something
}

koa中間件統一catch住Error,並返回相應code,msg

app.use((ctx, next) => {
  return next().catch((err) => {
    let code = 500
    let msg = 'unknown error'
    if (err instanceof CustomError || err instanceof HttpError) {
      const res = err.getCodeMsg()
      ctx.status = err instanceof HttpError ? res.code : 200
      code = res.code
      msg = res.msg
    } else {
      console.error('err', err)
    }
    ctx.body = {
      code,
      msg
    }
  })
})

經過以上4步,拋出異常只用一行代碼就搞定。

  • 拋出http錯誤就 throw new HttpError(HTTP_CODE.FORBIDDEN)
  • 拋出統一的業務錯誤碼就throw new CustomError(CUSTOM_CODE.SOME_CUSTOM_ERROR)
  • 拋出特殊錯誤就使用assert斷言庫

錯誤拋出後,會統一由koa中間件來處理。經過對Error的繼承,咱們將錯誤細分爲http error和業務錯誤,從而能夠更好地處理錯誤返回。

日誌

日誌咱們使用 - log4js 插件
在異常捕獲中間件進行存儲

var log4js = require('log4js')
log4js.configure({
  appenders: { koa: { type: 'file', filename: 'koa.log' } },
  categories: { default: { appenders: ['koa'], level: 'error' } }
});
const logger = log4js.getLogger('koa');
logger.error({url: ctx.request.url, error: err, params: ctx.request.body});

出現異常時就會生成日誌文件,如圖

最後

努力學習,提升代碼水平,少出異常!

相關文章
相關標籤/搜索