Java自学者论坛

 找回密码
 立即注册

手机号码,快捷登录

恭喜Java自学者论坛(https://www.javazxz.com)已经为数万Java学习者服务超过8年了!积累会员资料超过10000G+
成为本站VIP会员,下载本站10000G+会员资源,会员资料板块,购买链接:点击进入购买VIP会员

JAVA高级面试进阶训练营视频教程

Java架构师系统进阶VIP课程

分布式高可用全栈开发微服务教程Go语言视频零基础入门到精通Java架构师3期(课件+源码)
Java开发全终端实战租房项目视频教程SpringBoot2.X入门到高级使用教程大数据培训第六期全套视频教程深度学习(CNN RNN GAN)算法原理Java亿级流量电商系统视频教程
互联网架构师视频教程年薪50万Spark2.0从入门到精通年薪50万!人工智能学习路线教程年薪50万大数据入门到精通学习路线年薪50万机器学习入门到精通教程
仿小米商城类app和小程序视频教程深度学习数据分析基础到实战最新黑马javaEE2.1就业课程从 0到JVM实战高手教程MySQL入门到精通教程
查看: 590|回复: 0

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

[复制链接]
  • TA的每日心情
    奋斗
    2024-4-6 11:05
  • 签到天数: 748 天

    [LV.9]以坛为家II

    2034

    主题

    2092

    帖子

    70万

    积分

    管理员

    Rank: 9Rank: 9Rank: 9

    积分
    705612
    发表于 2021-8-31 09:22:09 | 显示全部楼层 |阅读模式

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

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

    哎...今天够累的,签到来了1...
    回复

    使用道具 举报

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    QQ|手机版|小黑屋|Java自学者论坛 ( 声明:本站文章及资料整理自互联网,用于Java自学者交流学习使用,对资料版权不负任何法律责任,若有侵权请及时联系客服屏蔽删除 )

    GMT+8, 2024-4-25 09:15 , Processed in 0.066609 second(s), 29 queries .

    Powered by Discuz! X3.4

    Copyright © 2001-2021, Tencent Cloud.

    快速回复 返回顶部 返回列表