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

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

例子是一个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这种低端浏览器也是兼容的,兼容性也有保障的。这里只是抛砖引玉,随着前端业务越来越复杂,统一的异常处理策略是非常必要的,实现方法肯定也会因项目而异的。

原文地址:https://www.cnblogs.com/laden666666/p/5449959.html