js构建ui的统一异常处理方案(四)

来源:互联网 发布:淘宝静物拍摄怎么做 编辑:程序博客网 时间:2024/06/05 01:10

上一篇我们介绍了统一异常处理方案的设计方案,这一篇我们将直接做一个小例子,验证我们的设计方案。

例子是一个tudo的列表界面(页面代码参考于),里面的各个按钮都会抛出不同的系统异常,从中我们可以测试各个异常的处理策略。例子中我们为了使其尽量能够兼容更多的浏览器(主要是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则暴漏给用户,用户必须让自己定义的异常类,寄生组合继承于此类。

三、统一异常处理的使用

每一个边界类中的动作都要用$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将第三方的插件封装。

上边的代码中,我们定义服务器的错误协议是以“{"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这种低端浏览器也是兼容的,兼容性也有保障的。这里只是抛砖引玉,随着前端业务越来越复杂,统一的异常处理策略是非常必要的,实现方法肯定也会因项目而异的。

0 0