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這種低端瀏覽器也是兼容的,兼容性也有保障的。這里只是拋磚引玉,隨着前端業務越來越復雜,統一的異常處理策略是非常必要的,實現方法肯定也會因項目而異的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM