上一篇我们介绍了统一异常处理方案的设计方案,这一篇我们将直接做一个小例子,验证我们的设计方案。
例子是一个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这种低端浏览器也是兼容的,兼容性也有保障的。这里只是抛砖引玉,随着前端业务越来越复杂,统一的异常处理策略是非常必要的,实现方法肯定也会因项目而异的。