重構 - 設計API的擴展機制

1.前言

上篇文章,主要介紹了重構的一些概念和一些簡單的實例。這一次,詳細的說下項目中的一個重構場景--給API設計擴展機制。目的就是爲了方便之後能靈活應對需求的改變。固然了,是否須要設計擴展性這個要看API的需求。若是你們有什麼建議,歡迎評論留言。javascript

2.擴展性表現形式

2-1.prototype

這個能夠說是JS裏面最原的一個擴展。好比原生JS沒有提供打亂數組順序的API,可是開發者又想方便使用,這樣的話,就只能擴展數組的prototype。代碼以下html

//擴展Array.prototype,增長打亂數組的方法。
Array.prototype.upset=function(){
    return this.sort((n1,n2)=>Math.random() - 0.5);
}

let arr=[1,2,3,4,5];
//調用
arr.upset();
//顯示結果
console.log(arr);複製代碼

運行結果vue

clipboard.png

功能是實現了。可是上面的代碼,只想借用例子講解擴展性,你們看下就好。不要模仿,也不要在項目這樣寫。如今基本都禁止這樣開發了。理由也很簡單,以前的文章也有提到過。這裏重複一下。java

這樣就污染了原生對象Array,別人建立的Array也會被污染,形成沒必要要的開銷。最可怕的是,萬一本身命名的跟原生的方法重名了,就被覆蓋原來的方法了。jquery

Array.prototype.push=function(){console.log('守候')}  
let arrTest=[123]
arrTest.push()
//result:守候
//push方法有什麼做用,你們應該知道,不知道的能夠去w3c看下複製代碼

clipboard.png

2-2.jQuery

關於 jQuery 的擴展性,分別提供了三個API:$.extend()、$.fn和$.fn.extend()。分別對jQuery的自己,靜態方法,原型對象進行擴展,基於jQuery寫插件的時候,最離不開的應該就是$.fn.extend()。vue-router

參考連接:設計模式

理解jquery的$.extend()、$.fn和$.fn.extend()
Jquery自定義插件之$.extend()、$.fn和$.fn.extend()數組

2-3.VUE

對VUE進行擴展,引用官網(插件)的說法,擴展的方式通常有如下幾種:瀏覽器

1.添加全局方法或者屬性,如: vue-custom-element微信

2.添加全局資源:指令/過濾器/過渡等,如 vue-touch

3.經過全局 mixin 方法添加一些組件選項,如: vue-router

4.添加 Vue 實例方法,經過把它們添加到 Vue.prototype 上實現。

5.一個庫,提供本身的 API,同時提供上面提到的一個或多個功能,如 vue-router

基於VUE的擴展。在組件,插件的內容提供一個install方法。以下

clipboard.png

使用組件

clipboard.png

上面幾個擴展性的實例分別是原生對象,庫,框架的擴展,你們可能以爲有點誇誇而談,那下面就分享一個平常開發經常使用的一個實例。

3.實例-表單驗證

看了上面那些擴展性的實例,下面看下一個在平常開發使用得也不少的一個實例:表單驗證。這塊能夠說很簡單,可是作好,作通用不簡單。看了《JavaScript設計模式與開發實踐》,用策略模式對之前的表單驗證函數進行了一個重構。下面進行一個簡單的分析。

下面的內容,代碼會偏多,雖然代碼不難,但仍是強烈建議你們不要只看,要邊看,邊寫,邊調試,否則做爲讀者,極可能不知道個人代碼是什麼意思,很容易懵。下面的代碼會涉兩個知識:開放-封閉原則和策略模式,你們能夠自行了解。

3-1.原來方案

/** * @description 字段檢驗 * @param checkArr * @returns {boolean} */
function validateForm(checkArr){
    let _reg = null, ruleMsg, nullMsg, lenMsg;
    for (let i = 0, len = checkArr.length; i < len; i++) {
        //若是沒字段值是undefined,再也不執行當前循環,執行下一次循環
        if (checkArr[i].el === undefined) {
            continue;
        }
        //設置規則錯誤提示信息
        ruleMsg = checkArr[i].msg || '字段格式錯誤';
        //設置值爲空則錯誤提示信息
        nullMsg = checkArr[i].nullMsg || '字段不能爲空';
        //設置長度錯誤提示信息
        lenMsg = checkArr[i].lenMsg || '字段長度範圍' + checkArr[i].minLength + "至" + checkArr[i].maxLength;
        //若是該字段有空值校驗
        if (checkArr[i].noNull === true) {
            //若是字段爲空,返回結果又提示信息
            if (checkArr[i].el === "" || checkArr[i].el === null) {
                return nullMsg;
            }
        }
        //若是有該字段有規則校驗
        if (checkArr[i].rule) {
            //設置規則
            switch (checkArr[i].rule) {
                case 'mobile':
                    _reg = /^1[3|4|5|7|8][0-9]\d{8}$/;
                    break;
                case 'tel':
                    _reg = /^\d{3}-\d{8}|\d{4}-\d{7}|\d{11}$/;
                    break;
            }
            //若是字段不爲空,而且規則錯誤,返回錯誤信息
            if (!_reg.test(checkArr[i].el) && checkArr[i].el !== "" && checkArr[i].el !== null) {
                return ruleMsg;
            }
        }
        //若是字段不爲空而且長度錯誤,返回錯誤信息
        if (checkArr[i].el !== null && checkArr[i].el !== '' && (checkArr[i].minLength || checkArr[i].maxLength)) {
            if (checkArr[i].el.toString().length < checkArr[i].minLength || checkArr[i].el.toString().length > checkArr[i].maxLength) {
                return lenMsg;
            }
        }
    }
    return false;
}複製代碼

函數調用方式

let testData={
 phone:'18819323632',
 pwd:'112'
    }

    let _tips = validateForm([
        {el: testData.phone, noNull: true, nullMsg: '電話號碼不能爲空',rule: "mobile", msg: '電話號碼格式錯誤'},
        {el: testData.pwd, noNull: true, nullMsg: '密碼不能爲空',lenMsg:'密碼長度不正確',minLength:6,maxLength:18}
    ]);
    //字段驗證若是返回錯誤信息
    if (_tips) {
        alert(_tips);
    }複製代碼

3-2.存在問題

這樣方法,相信你們看的也難受,由於問題確實是比較多。

1.一個字段進入,可能要通過三種判斷(空值,規則,長度)。若是隻是一個簡單的電話號碼規則校驗,就要通過其餘兩種不必的校驗,形成沒必要要的開銷。運行的流程就如同下面。


2.規則校驗裏面,只有這幾種校驗,若是要增長其餘校驗,好比增長一個日期的規則,沒法完成。若是一直修改源碼,可能會致使函數巨大。

3.寫法不優雅,調用也不方便。

3-3.代替方案

針對上面2-2的三個問題,逐個進行改善。

由於調用方式就不方便,很難在不改變 validateForm 調用方式的同時,優化重構內部的代碼,又增長擴展性。重寫這個方法又不可能,由於有個別的地方已經使用了這個API,本身一個一個的改不現實,因此就不修改這個 validateForm,新建一個新的API:validate。在之後的項目上,也儘可能引導同事放棄 validateForm,使用新的API。

上面第一個,優化校驗規則,每次校驗(好比空值,長度,規則),都是一個簡單的校驗,再也不執行其餘不必的校驗。運行流程如同下面。


let validate = function (arr) {
    let ruleData = {
        /** * @description 不能爲空 * @param val * @param msg * @return {*} */
        isNoNull(val, msg){
            if (!val) {
                return msg
            }
        },
        /** * @description 最小長度 * @param val * @param length * @param msg * @return {*} */
        minLength(val, length, msg){
            if (val.toString().length < length) {
                return msg
            }
        },
        /** * @description 最大長度 * @param val * @param length * @param msg * @return {*} */
        maxLength(val, length, msg){
            if (val.toString().length > length) {
                return msg
            }
        },
        /** * @description 是不是手機號碼格式 * @param val * @param msg * @return {*} */
        isMobile(val, msg){
            if (!/^1[3-9]\d{9}$/.test(val)) {
                return msg
            }
        }
    }
    let ruleMsg, checkRule, _rule;
    for (let i = 0, len = arr.length; i < len; i++) {
        //若是字段找不到
        if (arr[i].el === undefined) {
            return '字段找不到!'
        }
        //遍歷規則
        for (let j = 0; j < arr[i].rules.length; j++) {
            //提取規則
            checkRule = arr[i].rules[j].rule.split(":");
            _rule = checkRule.shift();
            checkRule.unshift(arr[i].el);
            checkRule.push(arr[i].rules[j].msg);
            //若是規則錯誤
            ruleMsg = ruleData[_rule].apply(null, checkRule);
            if (ruleMsg) {
                //返回錯誤信息
                return ruleMsg;
            }
        }
    }
};
let testData = {
    name: '',
    phone: '18819522663',
    pw: 'asda'
}
//校驗函數調用
console.log(validate([
    {
        //校驗的數據
        el: testData.phone,
        //校驗的規則
        rules: [
            {rule: 'isNoNull', msg: '電話不能爲空'}, {rule: 'isMobile', msg: '手機號碼格式不正確'}
        ]
    },
    {
        el: testData.pw,
        rules: [
            {rule: 'isNoNull', msg: '電話不能爲空'},
            {rule:'minLength:6',msg:'密碼長度不能小於6'}
        ]
    }
]));複製代碼

上面這裏就完成了第一步的優化,進行第二步以前,你們想下,若是上面ruleData的規則不夠用,好比我想增長一個日期範圍的校驗,必需要修改ruleData,增長一個屬性。以下

let ruleData = {
    //以前的一些規則
    /** * @description 是不是日期範圍 * @param val * @param msg * @return {*} */
    isDateRank(val,msg) {
        let _date=val.split(',');
        if(new Date(_date[0]).getTime()>=new Date(_date[1]).getTime()){
            return msg;
        }
    }
}複製代碼

若是又有其它的規則,又得改這個,這樣就違反了開放-封閉原則。若是多人共用這個函數,規則可能會不少,ruleData會變的巨大,形成沒必要要的開銷。好比A頁面有金額的校驗,可是隻有A頁面有。若是按照上面的方式改,在B頁面也會加載金額的校驗規則,可是根本不會用上,形成資源浪費。

因此下面應用開放-封閉原則。給函數的校驗規則增長擴展性。在實操以前,你們應該會懵,由於一個函數,能夠進行校驗的操做,又有增長校驗規則的操做。一個函數作兩件事,就違反了單一原則。到時候也難維護,因此推薦的作法就是分接口作。以下寫法。

let validate = (function () {
    let ruleData = {
        /** * @description 不能爲空 * @param val * @param msg * @return {*} */
        isNoNull(val, msg){
            if (!val) {
                return msg
            }
        },
        /** * @description 最小長度 * @param val * @param length * @param msg * @return {*} */
        minLength(val, length, msg){
            if (val.toString().length < length) {
                return msg
            }
        },
        /** * @description 最大長度 * @param val * @param length * @param msg * @return {*} */
        maxLength(val, length, msg){
            if (val.toString().length > length) {
                return msg
            }
        },
        /** * @description 是不是手機號碼格式 * @param val * @param msg * @return {*} */
        isMobile(val, msg){
            if (!/^1[3-9]\d{9}$/.test(val)) {
                return msg
            }
        }
    }
    return {
        /** * @description 查詢接口 * @param arr * @return {*} */
        check: function (arr) {
            let ruleMsg, checkRule, _rule;
            for (let i = 0, len = arr.length; i < len; i++) {
                //若是字段找不到
                if (arr[i].el === undefined) {
                    return '字段找不到!'
                }
                //遍歷規則
                for (let j = 0; j < arr[i].rules.length; j++) {
                    //提取規則
                    checkRule = arr[i].rules[j].rule.split(":");
                    _rule = checkRule.shift();
                    checkRule.unshift(arr[i].el);
                    checkRule.push(arr[i].rules[j].msg);
                    //若是規則錯誤
                    ruleMsg = ruleData[_rule].apply(null, checkRule);
                    if (ruleMsg) {
                        //返回錯誤信息
                        return ruleMsg;
                    }
                }
            }
        },
        /** * @description 添加規則接口 * @param type * @param fn */
        addRule:function (type,fn) {
            ruleData[type]=fn;
        }
    }
})();
//校驗函數調用-測試用例
console.log(validate.check([
    {
        //校驗的數據
        el: testData.mobile,
        //校驗的規則
        rules: [
            {rule: 'isNoNull', msg: '電話不能爲空'}, {rule: 'isMobile', msg: '手機號碼格式不正確'}
        ]
    },
    {
        el: testData.password,
        rules: [
            {rule: 'isNoNull', msg: '電話不能爲空'},
            {rule:'minLength:6',msg:'密碼長度不能小於6'}
        ]
    }
]));
//擴展-添加日期範圍校驗
validate.addRule('isDateRank',function (val,msg) {
    if(new Date(val[0]).getTime()>=new Date(val[1]).getTime()){
        return msg;
    }
});
//測試新添加的規則-日期範圍校驗
console.log(validate.check([
    {
        el:['2017-8-9 22:00:00','2017-8-8 24:00:00'],
        rules:[{
            rule:'isDateRank',msg:'日期範圍不正確'
        }]
    }
    
]));複製代碼

如上代碼所示,這裏須要往ruleData添加日期範圍的校驗,這裏能夠添加。可是不能訪問和修改ruleData的東西,有一個保護的做用。還有一個就是,好比在A頁面添加日期的校驗,只在A頁面存在,不會影響其它頁面。若是日期的校驗在其它地方均可能用上,就能夠考慮,在全局裏面爲ruleData添加日期的校驗的規則。

至於第三個問題,這樣的想法,可能不算太優雅,調用也不是太方便,可是就我如今能想到的,這個就是最好方案啊了。

這個看似是已經作完了,可是你們可能以爲有一種狀況沒能應對,好比下面這種,作不到。

clipboard.png

由於上面的check接口,只要有一個錯誤了,就立馬跳出了,不會校驗下一個。若是要實現下面的功能,就得實現,若是有一個值校驗錯誤,就記錄錯誤信息,繼續校驗下一個,等到全部的校驗都執行完了以後,以下面的流程圖。


執行完了,再把結果一塊兒返回,那麼下面還得暴露一個接口。

代碼以下(你們先忽略alias這個屬性)

let validate= (function () {
    let ruleData = {
        /** * @description 不能爲空 * @param val * @param msg * @return {*} */
        isNoNull(val, msg){
            if (!val) {
                return msg
            }
        },
        /** * @description 最小長度 * @param val * @param length * @param msg * @return {*} */
        minLength(val, length, msg){
            if (val.toString().length < length) {
                return msg
            }
        },
        /** * @description 最大長度 * @param val * @param length * @param msg * @return {*} */
        maxLength(val, length, msg){
            if (val.toString().length > length) {
                return msg
            }
        },
        /** * @description 是不是手機號碼格式 * @param val * @param msg * @return {*} */
        isMobile(val, msg){
            if (!/^1[3-9]\d{9}$/.test(val)) {
                return msg
            }
        }
    }
    return {
        check: function (arr) {
            //代碼不重複展現,上面一部分
        },
        addRule:function (type,fn) {
            //代碼不重複展現,上面一部分
        },
        /** * @description 校驗全部接口 * @param arr * @return {*} */
        checkAll: function (arr) {
            let ruleMsg, checkRule, _rule,msgArr=[];
            for (let i = 0, len = arr.length; i < len; i++) {
                //若是字段找不到
                if (arr[i].el === undefined) {
                    return '字段找不到!'
                }
                //若是字段爲空以及規則不是校驗空的規則

                //遍歷規則
                for (let j = 0; j < arr[i].rules.length; j++) {
                    //提取規則
                    checkRule = arr[i].rules[j].rule.split(":");
                    _rule = checkRule.shift();
                    checkRule.unshift(arr[i].el);
                    checkRule.push(arr[i].rules[j].msg);
                    //若是規則錯誤
                    ruleMsg = ruleData[_rule].apply(null, checkRule);
                    if (ruleMsg) {
                        //記錄錯誤信息
                        msgArr.push({
                            el:arr[i].el,
                            alias:arr[i].alias,
                            rules:_rule,
                            msg:ruleMsg
                        });
                    }
                }
            }
            //返回錯誤信息
            return msgArr.length>0?msgArr:false;
        }
    }
})();
let testData = {
    name: '',
    phone: '188',
    pw: 'asda'
}
//擴展-添加日期範圍校驗
validate.addRule('isDateRank',function (val,msg) {
    if(new Date(val[0]).getTime()>=new Date(val[1]).getTime()){
        return msg;
    }
});
//校驗函數調用
console.log(validate.checkAll([
    {
        //校驗的數據
        el: testData.phone,
        alias:'mobile',
        //校驗的規則
        rules: [
            {rule: 'isNoNull', msg: '電話不能爲空'}, {rule: 'isMobile', msg: '手機號碼格式不正確'},{rule:'minLength:6',msg: '手機號碼不能少於6'}
        ]
    },
    {
        el: testData.pw,
        alias:'pwd',
        rules: [
            {rule: 'isNoNull', msg: '電話不能爲空'},
            {rule:'minLength:6',msg:'密碼長度不能小於6'}
        ]
    },
    {
        el:['2017-8-9 22:00:00','2017-8-8 24:00:00'],
        rules:[{
            rule:'isDateRank',msg:'日期範圍不正確'
        }]
    }
]));複製代碼

看到結果,如今全部的不合法的數據的記錄都返回回來了。至於當時alias如今揭曉用處。
好比頁面是vue渲染的,根據alias能夠這樣處理。



若是是jQuery渲染的,根據alias能夠這樣處理。



3-4.向下兼容方案

由於項目以前有使用了之前的校驗 API,不能一刀切,在之前的 API 沒廢棄以前,不能影響使用。因此要重寫之前的 validateForm,使之兼容如今的新 API : validate 。

let validateForm=function (arr) {
        let _param=[],_single={};
        for(let i=0;i<arr.length;i++){
            _single={};
            _single.el=arr[i].el;
            _single.rules=[];
            //若有有非空檢驗
            if(arr[i].noNull){
                _single.rules.push({
                    rule: 'isNoNull',
                    msg: arr[i].nullMsg||'字段不能爲空'
                })
            }
            //若是有最小長度校驗
            if(arr[i].minLength){
                _single.rules.push({
                    rule: 'minLength:'+arr[i].minLength,
                    msg: arr[i].lenMsg ||'字段長度範圍錯誤'
                })
            }
            //若是有最大長度校驗
            if(arr[i].maxLength){
                _single.rules.push({
                    rule: 'maxLength:'+arr[i].maxLength,
                    msg: arr[i].lenMsg ||'字段長度範圍錯誤'
                })
            }
            //若是有規則校驗
            //校驗轉換規則
            let _ruleData={
                mobile:'isMobile'
            }
            if(arr[i].rule){
                _single.rules.push({
                    rule: _ruleData[arr[i].rule],
                    msg: arr[i].msg ||'字段格式錯誤'
                })
            }
            _param.push(_single);
        }
        let _result=validate.check(_param);
        return _result?_result:false;
    }
    let testData={
        phone:'18819323632',
        pwd:'112'
    }
    let _tips = validateForm([
        {el: testData.phone, noNull: true, nullMsg: '電話號碼不能爲空',rule: "mobile", msg: '電話號碼格式錯誤'},
        {el: testData.pwd, noNull: true, nullMsg: '密碼不能爲空',lenMsg:'密碼長度不正確',minLength:6,maxLength:18}
    ]);
    console.log(_tips)複製代碼

4.小結

今天的例子就到這裏了,這個例子,無非就是給 API 增長擴展性。這個例子比較簡單,不算難。你們用這個代碼在瀏覽器上運行,就很好理解。若是你們對這個例子有什麼更好的建議,或者代碼上有什麼問題,歡迎在評論區留言,你們多交流,相互學習。



-------------------------華麗的分割線--------------------

想了解更多,關注關注個人微信公衆號:守候書閣

clipboard.png

相關文章
相關標籤/搜索