上一篇我們介紹了統一異常處理方案的設計方案,這一篇我們將直接做一個小例子,驗證我們的設計方案。
例子是一個todo的列表界面(頁面代碼參考於https://github.com/zongxiao/Django-Simple-Todo),里面的各個按鈕都會拋出不同的系統異常,從中我們可以測試各個系統異常的處理策略。例子中我們為了使其盡量能夠兼容更多的瀏覽器(主要是ie8),同時保留mvvm、模塊化等如今前端開發的精華,所以采用avalon做view層和controller層,requirejs做模塊化工具實現自動加載資源和service的享元模式,樣式庫采用兼容ie8的bootstarp2。由於jquery1.x和jquery2.x對於promise/A+的規范實現的並不完整,故采用剛剛出爐的jquery-compat-3.0.0-alpha1版,不過要注意的是這是一個內部測試版。
demo的地址是: https://github.com/laden666666/UnifiedExceptionHandlingDome
一、對promise的封裝
從第二篇和第三篇可以看出,promise是統一異常處理的核心之一,因此需要對promise做出必要的封裝。
/** * $def是對$.Deferred的一些封裝,用於簡化我的的異步調用過程。同時promise的具體實現往往是參考promise/A+規范的,所以可以把此規范看做是一個門面模式 * 而$def可以看成是一個將具體實現封裝起來的適配器接口,可以讓不同對promise/A+規范實現的類庫都能被使用。因此用$def開發的的代碼將來即使使用其他類庫的 * promise實現代替$.Deferred的實現,這些代碼也可以很好的移植。所以$def產生的promise對象,建議僅使用resolve、reject和notify這幾個方法,因為 * 這些方法是標准promise提供的,更加利於代碼移植。 */ define("$def",['$'],function($) { window.$def = { /** * 快速resolve * @param {Object} o 返回的參數 */ resolve: function(o){ var d = $.Deferred(); d.resolve(o); return d.promise(); }, /** * 快速reject * @param {Object} o 拋出的異常 */ reject: function(o){ var d = $.Deferred(); d.reject(o); return d.promise(); }, /** * 對Promise/A+中的racte的實現 * @param {arguments} 一個Promise的數組 */ racte : function(){ var self = this; var d = $.Deferred(); $.each(arguments,function(i,e){ self.resolve() .then(function(){ return e; }) .then(function(){ d.resolve.apply(d,arguments); },function(err){ d.reject(err); }) }); return d.promise(); }, /** * 對Promise/A+中的all的實現 * @param {arguments} 一個Promise的數組 */ all : function(){ var list = []; for(var index in arguments){ list.push(this.resolve(arguments[index])); } return $.when.apply($,list); }, /** * 對ES6的Promise的實現 * @param {Function} fn 和標准的Promise的回調入參一樣,是兩個函數,分別是resolve和reject */ Promise : function(fn){ var d = $.Deferred(); function resolve(v){ d.resolve(v); } function reject(v){ d.reject(v); } if($.isFunction(fn)){ fn(resolve,reject) } return d.promise(); } } return window.$def; });
這樣,就簡化了promise的創建過程。為了將來能夠使用其他的promise類庫能夠代替 $.Deferred,更加利於代碼移植,我們的promise需要全部使用$def來創建,並且統一使用then,而不能使用fail這種不符合promise/A+的語法。
二、統一異常處理模塊
這個模塊共分為兩個部分,一個是創建系統異常的工廠模塊;另一個是實現異常處理策略注冊和處理的管理模塊。
define("errorManager",['$','$def'],function($,$def) {
//errorFactory注冊的異常
var errorList = {};
//對外暴漏的對象,負責注冊異常的處理策略,調用已經注冊的系統異常處理
var errorManager = {
/**
* 注冊異常,將類放入error列表中,並讓注冊異常的處理函數
* @param {Object} name 異常的名字
* @param {Object} handle 異常的處理函數
*/
registerError:function(name,handle){
if(!$.isFunction(handle)){
throw new Error("handle is not function");
}
//注冊
errorList[name] = {
handle : handle
}
},
/**
* 判斷異常是否是指定異常類
* @param {Object} error 需要判斷的異常對象
* @param {Object} errorName 異常的名字
*/
isError:function(error,errorName){
return error && error._errorName == errorName;
},
/**
* 判斷異常是否是指定異常類
* @param {Object} errorName 異常的名字
*/
findError:function(errorName){
return errorList[errorName];
},
/**
* 處理錯誤,根據不同的異常類型,使用注冊的異常方法處理去處理異常。這個就是在邊界類上進行統一異常處理的方法
* @param {Object} error 需要處理的異常
* @param {Object} defaultHandle 當異常和所有注冊的異常都不匹配的時候,做出的默認處理。這個參數可以是一個字符串,也可以是函數。如果是字符串就alert這個字符串,函數就執行這個函數
*/
handleErr : function(otherHandle,error){
if(!error || !error._errorName || !this.findError(error._errorName)){
//發現error是未注冊異常時候調用的方法
if($.isFunction(otherHandle)){
otherHandle(error);
} else {
console.error(error);
alert(otherHandle);
}
} else {
error.printStack();
//將錯誤源和系統默認的錯誤處理方法,都傳遞給注冊的異常處理方法
this.findError(error._errorName).handle(error,function(){
if($.isFunction(otherHandle)){
otherHandle(error);
} else {
console.log(otherHandle);
alert(otherHandle);
}
});
}
},
/**
* 訪問所有已注冊的異常的迭代器
*/
iterator:function(){
var list = [];
for(var k in errorList){
list.push(errorList[k]);
}
var i = 0;
return {
hasNext : function(){
return i < list.length;
},
next: function(){
var nextItem = list[i];
i++;
return nextItem;
},
reset : function(){
i = 0;
}
}
},
}
return errorManager;
});
/**
* 異常的創建工廠,同時提供注冊新的異常類方法
*/
define("errorFactory",['errorManager'],function(errorManager) {
var errorFactory = {};
//系統異常超類
errorFactory.BaseException = function (name,err) {
//error是真正的錯誤,記錄着調用的堆棧信息
this.error = new Error(err);
//異常的名字
this._errorName = name;
};
errorFactory.BaseException.prototype = {
printStack : function(){
//對於ie8這種不支持console的瀏覽器兼容
if(!window.console){
window.console = (function(){
var c = {}; c.log = c.warn = c.debug = c.info = c.error = c.time = c.dir = c.profile
= c.clear = c.exception = c.trace = c.assert = function(){};
return c;
})()
}
console.error(this.error.stack);
},
};
/**
* 寄生組合繼承實現,為了能實現堆棧信息的保留,使用這種特殊的js原型繼承模式。
* 如果使用簡單的prototype = new Error()的繼承模式。Error的堆棧信息永遠指向這個文件,
* 而不能把真正錯誤的語句的代碼位置顯示出來,故使用“寄生組合繼承”這種繼承方式
*/
function inheritPrototype(subType, superType) {
function F() {}
F.prototype = superType.prototype;
var prototype = new F();
prototype.constructor = subType;
subType.prototype = prototype;
}
//注冊的幾個系統異常
/**
* 用戶取消異常
* @param {Object} err 錯誤源
*/
function UserCancelException(err) {
errorFactory.BaseException.call(this,"userCancel",err);
}
inheritPrototype(UserCancelException,errorFactory.BaseException);
errorFactory.userCancel = function(err){
throw new UserCancelException(err);
}
function UserCancelHandle(err) {
//用戶取消異常,什么也不做
}
errorManager.registerError("userCancel",UserCancelHandle);
/**
* 初始化異常
* @param {Object} level 錯誤的級別
* @param {Object} err 錯誤源
*/
function InitException(level,err) {
errorFactory.BaseException.call(this,"init",err);
this.level = level;
}
inheritPrototype(InitException,errorFactory.BaseException);
errorFactory.InitCancel = function(level,err){
throw new InitException(level,err);
}
function InitHandle(err) {
//根據不同的錯誤級別做出不同的處理
switch (err.level){
default:
//根據不同的錯誤級別做出不同的處理策略,這里僅給出錯誤提示
alert("應用初始化時發生錯誤!");
break;
}
}
errorManager.registerError("init",InitHandle);
/**
* 網絡異常
* @param {Object} err 錯誤源
*/
function HttpException(err) {
errorFactory.BaseException.call(this,"http",err);
}
inheritPrototype(HttpException,errorFactory.BaseException);
errorFactory.http = function(err){
throw new HttpException(err);
}
function HttpHandle(err) {
//提示鏈接不到服務器
alert("無法訪問到服務器!");
}
errorManager.registerError("http",HttpHandle);
/**
* 服務器異常,如果服務器傳來了服務器錯誤信息,就提示服務器錯誤信息,否則就執行默認的錯誤提示
* @param {String} serverMsg 服務器端發來的錯誤提示
* @param {Object} err 錯誤源
*/
function ServerException(serverMsg,err) {
if(!err){
err = serverMsg;
} else {
this.serverMsg = serverMsg;
}
errorFactory.BaseException.call(this,"server",err);
}
inheritPrototype(ServerException,errorFactory.BaseException);
errorFactory.server = function(serverMsg,err){
throw new ServerException(serverMsg,err);
}
function ServerHandle(err,defaultHandle) {
//提示鏈接不到服務器
if(err.serverMsg ){
alert(err.serverMsg);
} else {
defaultHandle();
}
}
errorManager.registerError("server",ServerHandle);
return errorFactory;
});
異常的統一處理函數是errorManager.handleErr(otherHandle,error)。這個方法要求用戶傳遞一個默認的提示語句或者異常處理函數,如果異常不能使用已經注冊的處理方法處理,就使用這個默認的處理策略,否則就按照注冊的處理策略去處理異常。
在errorFactory中,定義了幾種系統異常。這些異常繼承方式采用寄生組合繼承,這個繼承方法沒有對外暴漏,用戶要注冊自己的異常的話,需要自己實現寄生組合繼承。而異常的原型errorFactory.BaseException則暴漏給用戶,用戶必須讓自己定義的異常類,寄生組合繼承於此類。
三、統一異常處理的使用
每一個controller中的事件都要用$def.resolve()開頭,這樣主要是防止第一個promise創建之前也會出現異常,我們用一個promise把所有的代碼包含進入,這樣就不用擔心在promise創建之前會出現異常的情況了。在最后一步我們去catch這個promise的所拋出的異常(如果有的話),用then(null,onreject)語句去捕獲異常,因為各個promise庫對捕獲語句的關鍵字定義不同(如jq是用fail,而angular是用catch),所以使用then是兼容性是最好的寫法。
一個標准的模板代碼塊如下:
return $def.resolve() .then(function(){ //業務代碼 }) .then(null,function(err){ //調用統一異常處理,處理異常情況 eM.handleErr("默認的異常處理語句",err); });
以下是例子中controller的代碼:
//創建avalon的controller和定義vm var todoController = avalon.define({ $id: "todo", //todo的列表 todolist : [], //刪除todo deleteTodo : function(todo){ return $def.resolve() .then(function(){ if(!confirm("確定要刪除嗎?")){ //直接拋出用戶取消異常,這樣不用管后面邏輯如何,都會進入handleErr里。而用戶取消異常的handleErr什么都不做 eF.userCancel(); } }) .then(function(){ return todoService.deleteTodo(todo.id); }).then(null,function(err){ //調用統一異常處理,處理異常情況 eM.handleErr("刪除todo提交失敗!",err); }); }, //完成todo finishTodo : function(todo){ return $def.resolve() .then(function(){ return todoService.finishTodo(todo.id); }).then(null,function(err){ //調用統一異常處理,處理異常情況 eM.handleErr("完成todo提交失敗!",err); }); }, //重做todo redoTodo : function(todo){ return $def.resolve() .then(function(){ return todoService.redoTodo(todo.id); }).then(null,function(err){ //調用統一異常處理,處理異常情況 eM.handleErr("重做todo提交失敗!(這個是默認的提示)",err); }); }, });
上述代碼中deleteTodo、finishTodo 和redoTodo 三個函數就是頁面事件的響應函數,只需在這里使用統一異常處理就完成了所有的異常處理了。統一異常處理的核心就是在邊界類中做統一的一次異常處理,而處理的對象就是底層代碼無法處理的異常。事實上實際代碼開發中,絕大部分異常都是底層代碼無法處理的,需要向上拋出,而使用統一異常處理后異常處理代碼就變得非常簡單了。
四、幾種系統異常的封裝
同時,我們需要將一些特定異常包裝成系統異常,這些在上一篇有提及,具體實現如下:
1.用戶取消異常
這是一個使用頻率比較高的異常,用戶所有的取消動作都可以讓其拋出這個異常。如下面代碼:
//刪除todo deleteTodo : function(todo){ return $def.resolve() .then(function(){ if(!confirm("確定要刪除嗎?")){ //直接拋出用戶取消異常,這樣不用管后面邏輯如何,都會進入handleErr里。而用戶取消異常的handleErr什么都不做 eF.userCancel(); } }) .then(function(){ return todoService.deleteTodo(todo.id); }).then(null,function(err){ //調用統一異常處理,處理異常情況 eM.handleErr("刪除todo提交失敗!",err); }); },
當用戶取消異常拋出之后,就會直接進入到catch語句中的handleErr里,而我們在handleErr里注冊的策略是什么也沒有做,不會寫日志或者彈出錯誤警告。這樣我們不用專門為用戶取消事件去寫一個分支,處理起來清晰簡單。
2.網絡異常和服務器異常
這兩個異常都是對http請求中的響應封裝。網絡異常需要大家精通http協議,知道什么錯誤是網絡本身引起的。服務器異常還需要我們和服務器建立一個協議,這樣能夠獲得服務器拋出的異常信息(如果這個信息有必要給用戶看)。所以這兩個請求都需要對ajax進行封裝,封裝的事例如下:
/** * 基於jq負責發送ajax的方法 */ define("$ajax",['$','errorFactory'],function($,eF) { return function(option){ return $.ajax(option).promise() //將失敗的ajax調用封裝成 .then(null,function(err){ //如果是status為0,表示超時取消或者ajax終止,提交http請求異常。如果狀態為502是網關錯誤,表示當前網路還是連接不上服務器 if(err.status == 0 || err.status == 502){ throw eF.http(err); } else{ //否則,需要根據服務器端做好接口,通過responseText判斷出是服務器端異常,把服務器端傳遞來的消息提示出去 //這里只是示意的代碼,需要根據服務器端具體情況具體處理 if(err.responseText.indexOf("{\"msg\":") == 0){ throw eF.server(JSON.parse(err.responseText).msg ,err); } //以上情況都不符合,直接把原始異常向上拋出 throw err; } }); } });
起初我准備設置$.ajax默認的error事件,在那里把原始異常封裝,但是后來發現在error事件中拋出的錯誤無法拋給promise里,所以我們只能直接對promise進行catch,將異常包裝一下。這樣如果用戶是使用$ajax請求的異步處理都可以自動地封裝成兩個異常。不過這樣也有個缺點,就是第三方的應用的ajax不能被自動封裝,因為他們使用的是jq的$.ajax接口,所有需要我們自己去用promise將第三方的插件封裝。這一點jq可以改進一下,提供一個類似beforeSend的beforeError方法,或者能夠把error的錯誤拋到promise里。
上邊的代碼中,我們定義服務器的錯誤協議是以“{"msg":”開頭才行,而不符合這個協議的異常全部以原始異常的形式向上拋出。
3.表單的異常
很遺憾由於時間的關系我們沒有把表單異常的處理方案分享給大家,主要是表單異常處理起來是很麻煩的。表單異常其實就是表單校驗的錯誤,而表單校驗一部分是屬於view層負責的功能,例如必填項,或者是內容的正則判斷,這些在視圖層上完成最適合了;但是還是有一部分卻是需要和后台交互,是service層的業務,例如從服務器中查詢用戶名和密碼是否正確的登錄驗證,這樣我們需要在controller層將這種錯誤封裝為表單異常,在拋給統一異常處理,而統一異常處理也需要使用和視圖層相同的方式去提示錯誤,因此表單異常處理本身也需要支持錯誤處理策略的注冊功能。整個過程涉及到mvc的各個層次,這個就留給大家自己去實現吧。
4.非系統異常
我們每一個統一異常處理(handleErr)的調用,都會有一個默認的處理方法,這個可以一個字符串,也可以是一個function,他們是用於統一異常處理無法找到注冊的系統異常handle去處理異常時候調用的方法。當出現非系統異常的時候,我們handleErr還是可以采用一種默認的異常提示方案。事實上實際項目中,系統異常並不多,大多數都是那些無法被包裝成系統異常的異常。對於這種異常,一定要把錯誤的源打印到日志里,這樣才能方便大家調試。
例如demo中的redoTodo事件,底層todoService.redoTodo方法拋出的是非系統異常,所以錯誤提示會顯示eM.handleErr第一個參數提供的默認的提示語句。
//重做todo redoTodo : function(todo){ return $def.resolve() .then(function(){ return todoService.redoTodo(todo.id); }).then(null,function(err){ //調用統一異常處理,處理異常情況 eM.handleErr("重做todo提交失敗!(這個是默認的提示)",err); }); },
5.自定義系統異常
所有異常的原型errorFactory.BaseException是暴漏給用戶了,所有用戶可以自己去注冊自己的異常處理方案。這個demo的注冊代碼和異常的寄生組合繼承過程有點復雜,是可以簡化的,這個也留給大家自己去探索如何去簡化異常的繼承和注冊吧。自定義異常的具體注冊過程可以參考errorFactory中的系統異常定義。
五、總結
我們項目使用了統一異常處理策略后,分層實現起來更簡單了,每一層的代碼只需要思考自己正確的業務邏輯,遇到錯誤就直接向上拋出,是符合責任鏈模式的;同時異常提示也做的更准確了,基本上每一個錯誤都能提示給用戶,不會出現系統提示成功,而實際上卻是錯誤的情況。
雖然統一的異常處理策略實現起來成本比較高,但是還是很有實現意義的,而且即便是ie8這種低端瀏覽器也是兼容的,兼容性也有保障的。這里只是拋磚引玉,隨着前端業務越來越復雜,統一的異常處理策略是非常必要的,實現方法肯定也會因項目而異的。
