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