軟件開發時,有 80% 的代碼在處理各類錯誤。
javascript——某著名開發者html
想讓本身的代碼健壯,錯誤處理是必不可少的。這篇文章將主要介紹 koa 框架中錯誤處理的實現(其實主要是 co 的實現),使用 koa 框架開發 web 應用時進行錯誤處理的一些方法。java
在 Node 中,錯誤處理的方法主要有下面幾種:node
和其餘同步語言相似的 throw / try / catch 方法git
callback(err, data) 回調形式es6
經過 EventEmitter 觸發一個 error 事件github
第一種使用 catch 來捕獲錯誤,十分易用,其餘兩種在捕獲錯誤時多多少少都有些彆扭。web
可是 koa 經過十分巧妙的」黑魔法「讓咱們可使用 catch 來捕獲異步代碼中的錯誤。好比下面的例子:數據庫
const fs = require('fs'); const Promise = require('bluebird'); let filename = '/nonexists'; let statAsync = Promise.promisify(fs.stat); try { yield statAsync(filename); } catch(e) { // error here }
在 koa 中,推薦統一使用 throw / try / catch 的方式來進行錯誤的觸發和捕獲,這會讓代碼更加易讀,防止被繞暈。json
上面咱們說了 koa 中可使用 try / catch,咱們就來分析下它是如何作到的。koa 基於 co,因此,咱們其實主要是分析 co 的實現。(注:這一部分比較偏原理,不關心的能夠跳過。)
首先,咱們來看看什麼是 generator。
function* gen() { var a = yield 'start'; console.log(a); var b = yield 'end'; console.log(b); return 'over'; } var it = gen(); console.log(it.next()); // {value: 'start', done: false} console.log(it.next(22)); // 22 {value: 'end', done: false} console.log(it.next(333)); // 333 {value: 'over', done: true}
帶有 *
的函數聲明表示是一個 generator 函數,當執行 gen()
時,函數體內的代碼並無執行,而是返回了一個 generator 對象。
generator 函數一般和 yield 結合使用,函數執行到每一個 yield 時都會暫停並返回 yield 的右值。下次調用 next 時,函數會從 yield 的下一個語句繼續執行。等到整個函數執行完,next 方法返回的 done 字段會變成 true,而且將函數返回值做爲 value 字段。
第一次執行 next()
時,走到 yield 'start'
後暫停並返回 yield
的右值 'start'
。注意,此時var a =
這個賦值語句其實尚未執行。
第二次執行 next(22)
時,從 yield 'start'
下一個語句執行。因而執行 var a =
這個賦值語句,而表達式 yield 'start'
的值就等於傳遞給 next
函數的參數值 22
,因此,a
被賦值爲 22
。而後繼續往下執行到 yield 'end'
後暫停並返回 yield
的右值 'end'
。
第三次執行 next(333)
時,從 yield 'end'
下一個語句執行。此時執行 var b =
這個賦值語句,表達式 yield 'end'
的值等於傳遞給 next
函數的參數 333
,b
被賦值爲 333
。繼續往下執行到 return
語句,將 return
語句的返回值做爲 value
返回,由於函數已經執行完畢,done
字段標記爲 true
。
能夠看到 generator 就是一種迭代機制,就像一隻很懶的青蛙,戳一下(調用 next
)動一下。
generator 對象還有一個 throw
方法,能夠在 generator 函數外面拋出異常,而後在 generator 函數裏面捕獲異常。有點繞?咱們來看一個實例:
function *gen() { try { yield 'a'; yield 'b'; } catch(e) { console.log('inside:', e); // inside: [Error: error from outside] } } var it = gen(); it.next(); console.log(it.throw(new Error('error from outside'))); // { value: undefined, done: true }
咱們執行一次 next
,會運行到 yield 'a'
這裏而後暫停,這一句恰好在 try 的返回內,所以 it.throw
拋出的錯誤咱們能夠 catch 到。而且看到 throw
返回的 done
字段是 true
,說明後面的 yield 'b'
已經不會再執行了。
若是咱們不調用 next
,或者連續調用三次 next
,yield
代碼不在 try
返回裏面,會致使報錯。co 的錯誤處理其實正是利用了這個 throw
方法。
下面咱們來看看 co 的核心代碼:
function co(gen) { var ctx = this; var args = slice.call(arguments, 1); // 統一返回一個總體的 promise return new Promise(function(resolve, reject) { // 若是是函數,調用並取得 generator 對象 if (typeof gen === 'function') gen = gen.apply(ctx, args); // 若是根本不是 generator 對象(沒有 next 方法),直接 resolve 掉並返回 if (!gen || typeof gen.next !== 'function') return resolve(gen); // 入口函數 onFulfilled(); function onFulfilled(res) { var ret; try { // 拿到 yield 的返回值 ret = gen.next(res); } catch (e) { // 若是執行發生錯誤,直接將 promise reject 掉 return reject(e); } // 延續調用鏈 next(ret); } function onRejected(err) { var ret; try { // 若是 promise 被 reject 了就直接拋出錯誤 ret = gen.throw(err); } catch (e) { // 若是執行發生錯誤,直接將 promise reject 掉 return reject(e); } // 延續調用鏈 next(ret); } function next(ret) { // generator 函數執行完畢,resolve 掉 promise if (ret.done) return resolve(ret.value); // 將 value 統一轉換爲 promise var value = toPromise.call(ctx, ret.value); // 將 promise 添加 onFulfilled、onRejected,這樣當新的promise 狀態變成成功或失敗,就會調用對應的回調。整個 next 鏈路就執行下去了 if (value && isPromise(value)) return value.then(onFulfilled, onRejected); // 無法轉換爲 promise,直接 reject 掉 promise return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, ' + 'but the following object was passed: "' + String(ret.value) + '"')); } }); }
假設有下面的代碼,讓咱們一塊兒推演下執行流程:
co(function* gen() { var a = yield Promise.resolve('a 值'); console.log(a); try { var b = yield Promise.reject(new Error('b 錯誤')); var c = yield Promise.resolve('c 值'); console.log(b, c); } catch(e) { console.log('error', e); } return 'over'; }).then(function (value) { console.log(value); }).catch(function (err) { console.error(err.stack); });
約定:Promise.resolve('a 值')
生成的是 promiseA;Promise.reject(new Error('b 錯誤'))
生成的是 promiseB。
首先傳入 co 的 gen 函數會被執行,獲取到 generator 對象。對應代碼:if (typeof gen === 'function') gen = gen.apply(ctx, args);
。
而後調用 onFulfilled
函數。開啓整個執行過程。
第一次執行 ret = gen.next(res)
,走到 yield Promise.resolve('a 值')
後暫停並返回 yield
的右值,此時 ret
等於 {value: PromiseA, done: false}
。
而後執行 next(ret)
,將 ret.value
轉換爲 Promise,執行 value.then(onFulfilled, onRejected)
,也就是 PromiseA.then(onFulfilled, onRejected)
。當咱們的 PromiseA 被 resolve 後,又再次執行 onFulfilled
,並傳入 resvole 的值,也就是:onFulfilled('a 值')
。
因而第二次執行 ret = gen.next('a 值')
(此時的 res
就等於 a 值
),進入到 gen 函數,執行接下來的 var a =
賦值語句,yield Promise.resolve('a 值')
的返回值等於給 next
傳遞的參數 'a 值'
,因而變量 a
被賦值爲 'a 值'
。繼續執行到 yield Promise.reject(new Error('b 錯誤'))
後暫停並返回 yield
的右值,此時 ret
等於 {value: PromiseB, done: false}
。
繼續執行 next(ret)
,延續調用鏈。執行 value.then(onFulfilled, onRejected)
,也就是 PromiseB.then(onFulfilled, onRejected)
。此次 PromiseB 被 reject 掉了,因而執行 onRejected
,並傳人 reject 的錯誤緣由,也就是:onRejected(new Error('b 錯誤'))
。
因而執行到 ret = gen.throw(new Error('b 錯誤'))
,而此時 yield Promise.reject(new Error('b 錯誤'))
恰好在 try 的範圍內,錯誤被 catch 住了!接着就執行 catch 裏面的打印語句 console.log('error', e);
,一路執行到函數結束(由於再也沒有 yield
了),將返回值賦給 value
。最後 ret
等於 {value: 'over', done: true}
。
繼續執行 next(ret)
,延續調用鏈。執行到 if (ret.done) return resolve(ret.value);
,因而總體的 promise 被 resolve 掉,執行 then
裏面的打印語句,打印出 ret.value
的值 'over'
。整個流程結束。
若是咱們不 try / catch 會怎樣?由於 onRejected
裏面有是這樣處理的:try { ret = gen.throw(err); } catch (e) { return reject(e); }
。咱們上面說若是 yield
沒有在 try
裏會致使 gen.throw
報錯,因而總體 promise 被 reject,執行其 catch
方法,打印出 Error('b 錯誤')
的堆棧。
這就是「黑魔法」的神祕面紗!對 TJ 大神真是一個大寫的「服」字。
接下來的問題是什麼樣的錯誤咱們須要處理?怎麼處理?咱們能夠將錯誤分個類:
操做錯誤:不是程序 bug 致使的運行時錯誤。好比:鏈接數據庫服務器失敗、請求接口超時、系統內存用光等等。
程序錯誤:程序 bug 致使的錯誤,只要修改代碼就能夠避免。好比:嘗試讀取未定義對象的屬性、語法錯誤等等。
很顯然,咱們真正須要處理的是操做錯誤,程序錯誤應該立刻進行修復。
那怎麼處理操做錯誤呢?總結起來大概有下面這些方法:
直接處理。這個簡直是廢話。舉個例子:嘗試向一個文件中寫東西,可是這個文件不存在,那這個時候會報錯吧?處理這個錯誤的方法就是先建立好要寫入的文件。若是咱們知道怎麼處理錯誤,那直接處理就是。
重試。有時候某些錯誤多是偶發的(好比:鏈接的服務不穩定等),咱們能夠嘗試對當前操做進行重試。可是必定要設置重試的超時時間、次數,避免長時間的等待卡死應用。
直接將錯誤拋給調用方。若是咱們不知道具體怎麼處理錯誤,那最簡單的就是將錯誤往上拋。好比:檢查到用戶沒有權限訪問某個資源,那咱們直接 throw 一個 Error(並帶上 status 是 403)比較好,上層代碼能夠 catch 這個錯誤,而後要麼展現一個統一的無權限頁面給用戶,要麼返回一個統一的錯誤 json 給調用方。
寫日誌而後將錯誤拋出。這種狀況通常是發生了比較致命的錯誤,無法處理,也不能重試,那咱們須要記下錯誤日誌(方便之後定位問題),而後將錯誤往上拋(交給上層代碼去進行統一錯誤展現)。
有了上面的說明,那如今咱們就來看看在 koa 裏面怎麼優雅的實現統一錯誤處理。
答案就是使用強大的中間件!
咱們能夠在業務邏輯中間件(通常就是 MVC 中的 Controller)開始以前定義下面的中間件:
app.use(function* (next) { try { yield* next; } catch(e) { let status = e.status || 500; let message = e.message || '服務器錯誤'; if (e instanceof JsonError) { // 錯誤是 json 錯誤 this.body = { 'status': status, 'message': message }; if (status == 500) { // 觸發 koa 統一錯誤事件,能夠打印出詳細的錯誤堆棧 log this.app.emit('error', e, this); } return; } this.status = status; // 根據 status 渲染不一樣的頁面 if (status == 403) { this.body = yield this.render('403.html', {'err': e}); } if (status == 404) { this.body = yield this.render('404.html', {'err': e}); } if (status == 500) { this.body = yield this.render('500.html', {'err': e}); // 觸發 koa 統一錯誤事件,能夠打印出詳細的錯誤堆棧 log this.app.emit('error', e, this); } } });
能夠看到,咱們直接執行 yield* next
,而後 catch
執行過程當中任何一箇中間件的錯誤,而後根據錯誤的「特性」,分別進行不一樣的處理。
有了這個中間件,咱們的業務邏輯 controller 中的代碼就能夠這樣來觸發錯誤:
const router = new (require('koa-router')); router.get('/some_page', function* () { // 直接拋出錯誤,被中間件捕獲後當成 500 錯誤 throw new PageError('發生了一個致命錯誤'); throw new JsonError('發送了一個致命錯誤'); // 帶 status 的錯誤,被中間件捕獲後特殊處理 this.throw(403, new PageError('沒有權限訪問')); this.throw(403, new JsonError('沒有權限訪問')); });
上面的代碼裏面出現的 JsonError
、PageError
,其實是繼承於 Error
的兩個構造器。代碼以下:
const util = require('util'); exports.JsonError = JsonError; exports.PageError = PageError; function JsonError(message) { Error.call(this, message); } util.inherits(JsonError, Error); function PageError(message) { Error.call(this, message); } util.inherits(PageError, Error);
經過繼承 Error
構造器,咱們能夠將錯誤進行細分,從而能更精細的對錯誤進行處理。