5分鐘便可掌握的前端高效利器:JavaScript 策略模式

淺談 JavaScript 中策略模式的使用:

  • 什麼是設計模式
  • 什麼是策略模式
  • 策略模式在 JavaScript 中的應用(使用策略模式封裝百度AI識別調用)
  • 策略模式在 Vue 組件封裝中的應用(使用策略模式封裝Select組件)

什麼是設計模式

設想有一個電子愛好者,雖然他沒有通過正規的培訓,可是卻日積月累地設計並製造出了許多有用的電子設備:業餘無線電、蓋革計數器、報警器等。有一天這個愛好者決定從新回到學校去攻讀電子學學位,來讓本身的才能獲得正式的承認。隨着課程的展開,這個愛好者忽然發現課程內容都似曾相識。似曾相識的不是術語或表述的方式,而是背後的概念。這個愛好者不斷學到一些名稱和原理,雖然這些名稱和原理原來他並不知道,但事實上他多年以來一直都在使用。整個過程只不過是一個接一個的頓悟。

設計模式沉思錄 ,John Vlissides, 第一章 1.2節javascript

咱們在寫代碼的時候,必定也遇到過許多相似的場景。隨着經驗的增長,咱們對於這些常見場景的處理愈來愈駕輕就熟,甚至總結出了針對性的「套路」,下次遇到此類問題直接運用「套路」解決,省心又省力。這些在軟件開發過程當中逐漸積累下來的「套路」就是設計模式。html

設計模式的目標之一就是提升代碼的可複用性、可擴展性和可維護性。正因如此,雖然有時候咱們不知道某個設計模式,可是看了相關書籍或文章後會有一種「啊,原來這就是設計模式」的恍然大明白。vue

若是你看完這篇文章後也有此感受,那麼恭喜你,你已經在高效程序員的道路上一路狂奔了。java

什麼是策略模式

策略模式是一種簡單卻經常使用的設計模式,它的應用場景很是普遍。咱們先了解下策略模式的概念,再經過代碼示例來更清晰的認識它。git

策略模式由兩部分構成:一部分是封裝不一樣策略的策略組,另外一部分是 Context。經過組合和委託來讓 Context 擁有執行策略的能力,從而實現可複用、可擴展和可維護,而且避免大量複製粘貼的工做。程序員

策略模式的典型應用場景是表單校驗中,對於校驗規則的封裝。接下來咱們就經過一個簡單的例子具體瞭解一下:github

粗糙的表單校驗

一個常見的登陸表單代碼以下:ajax

<form id='login-form' action="" method="post">
    <label for="account">手機號</label>
    <input type="number" id="account" name="account">
    <label for="password">密碼</label>
    <input type="password" id="password" name="password">
    <button id='login'>登陸</button>
</form>
<script>
    var loginForm = document.getElementById('login-form');

    loginForm.onsubmit = function (e) {
        e.preventDefault();  
        var account = document.getElementById("account").value;
        var pwd = document.getElementById("password").value;

        if(account===null||account===''){
            alert('手機號不能爲空');
            return false;
        }
        if(pwd===null||pwd===''){
            alert('密碼不能爲空');
            return false;
        }
        if (!/(^1[3|4|5|7|8][0-9]{9}$)/.test(account)) {
            alert('手機號格式錯誤');
            return false;
        }
        if(pwd.length<6){
            alert('密碼不能小於六位');
            return false;
        }
        // ajax 發送請求
    }
</script>

以上代碼,雖然功能沒問題,可是缺點也很明顯:vuex

代碼裏遍地都是 if 語句,而且它們缺少彈性:每新增一種、或者修改原有校驗規則,咱們都必須去改loginForm.onsubmit內部的代碼。另外邏輯的複用性也不好:若是有其它表單也是用一樣的規則,這段代碼並不能複用,只能複製。當校驗規則發生變化時,好比上文的正則校驗並不能匹配虛擬運營商14/17號段,咱們就須要手動同步多處代碼變動(Ctrl+C/Ctrl+V)。element-ui

優秀的表單驗證

接下來咱們經過策略模式的思路改寫一下上段代碼,首先抽離並封裝校驗邏輯爲策略組:

var strategies = {
    isNonEmpty: function (value, errorMsg) {
        if (value === '' || value === null) {
            return errorMsg;
        }
    },
    isMobile: function (value, errorMsg) { // 手機號碼格式
        if (!/(^1[3|4|5|7|8][0-9]{9}$)/.test(value)) {
            return errorMsg;
        }
    },
    minLength: function (value, length, errorMsg) {
        if (value.length < length) {
            return errorMsg;
        }
    }
};

接下來修改 Context:

var loginForm = document.getElementById('login-form');

loginForm.onsubmit = function (e) {
    e.preventDefault(); 
    var accountIsMobile = strategies.isMobile(account,'手機號格式錯誤');
    var pwdMinLength = strategies.minLength(pwd,8,'密碼不能小於8位');
    var errorMsg = accountIsMobile||pwdMinLength; 
    if(errorMsg){
        alert(errorMsg);
        return false;
    }
}

對比兩種實現,咱們能夠看到:分離了校驗邏輯的代碼若是須要擴展校驗類型,在策略組中新增定義便可使用;若是須要修改某個校驗的實現,直接修改相應策略便可全局生效。對於開發和維護都有明顯的效率提高。

擴展:史詩的表單校驗

有興趣的朋友能夠了解下 async-validator ,element-ui 和 antd 的表單校驗都是基於 async-validator 封裝的,能夠說是史詩級別的表單校驗了

經過表單校驗的對比,相信你們都對策略模式有所瞭解,那麼接下來經過兩個例子具體瞭解下 JavaScript 中策略模式的應用:

使用策略模式調用百度AI圖像識別

由於百度AI圖像識別的接口類型不一樣,所需的參數格式也不盡相同。然而圖像的壓縮及上傳、錯誤處理等部分是公用的。因此能夠採用策略模式封裝:

定義策略組

經過定義策略組來封裝不一樣的接口及其參數:好比身份證識別接口的side字段,自定義識別的templateSign字段,以及行駛證識別的接收參數爲poparamstData

/**
 * 策略組
 * IDCARD:身份證識別
 * CUSTOMIZED:自定義識別
 * VL:行駛證識別
 */
var strategies = {
    IDCARD: function (base64) {
        return {
            path: 'idcard',
            param: {
                'side': 'front',
                'base64': base64
            }
        };
    },

    CUSTOMIZED: function (base64) {
        return {
            path: 'customized',
            param: {
                'templateSign': '52cc2d402155xxxx',
                'base64': base64
            }
        };
    },
    VL: function (base64) {
        return {
            path: 'vehicled',
            poparamstData: {
                'base64': base64
            }
        };
    },
};

定義 Context

var ImageReader = function () { };

/**
 * 讀取圖像,調用接口,獲取識別結果
 * 
 * @param {*} type 待識別文件類型
 * @param {*} base64 待識別文件 BASE64碼
 * @param {*} callBack 識別結果回調
 */
ImageReader.prototype.getOcrResult = function (type, base64, callBack) {
    let fileSize = (base64.length / (1024 * 1024)).toFixed(2);
    let compressedBase64 = '';
    let image = new Image();
    image.src = base64;
    image.onload = function () {
        /**
         * 圖片壓縮處理及異常處理,代碼略
         */
         

        let postData = strategies[type](compressedBase64);

        ajax(
            host + postData.path, {
                data: postData.param,
                type: 'POST',
                headers: {
                    'Content-Type': 'application/x-www-form-urlencoded'
                },
                success: function (res) {
                    var data = JSON.parse(res);
                    // 暴露給 UI 層的統一的錯誤碼
                    if (data.error_code !== undefined && data.error_code !== 0) {
                        var errorData = {
                            error: 1,
                            title: '錯誤 ' + data.error_code,
                            content: 'error message'
                        };
                        callBack(errorData);
                    } else {
                        callBack(data);
                    }
                }
            });
    };
};

調用方式

var imageReader = new ImageReader();
imageReader.getOcrResult('IDCARD', this.result.toString(), callback);

使用策略模式封裝 Vue Select 組件

某項目中多處用到了 element-ui 的 select 組件,其內在邏輯相似,都是初始化時獲取下拉列表的數據源,而後在選中某一項時 dispatch 不一樣的 action。遂考慮使用策略模式封裝。

Context

在本例中,組件向外部暴露一個 prop,調用方指定該 prop 從而加載不一樣的策略。那麼定義 Context 以下:

<template>
  <el-select v-model="selectedValue" placeholder="請選擇" @change="optionChanged" size="mini" clearable>
    <el-option v-for="item in options" :key="item.id" :label="item.name" :value="item.id">
    </el-option>
  </el-select>
</template>
data() {
    return {
      selectedValue: undefined,
      options: [],
      action: "",
    };
  },
  props: {
    // 暴露給外部的 select-type
    selectType: {
      type: String
    },
  },
  created() {
   // 獲取 options
   this.valuation();
  },
    methods: {
    optionChanged() {
      this.$emit(this.action, this.selectedValue);
    },
    setOptions(option) {
      this.$store.dispatch(this.action, option);
    },
    valuation() {
      // 獲取 options 數據
    }
  },

外部經過以下方式調用組件:

<MySelect selectType="product"/>

strategies

而後定義策略組:

let strategies = {
    source: {
        action: "sourceOption",
        getOptions:  function() {
            // 拉取 options
        }
    },
    product: {
        action: "productOption",
        getOptions:  function() {
            // 拉取 options
        }
    },
    ...
}

異步

至此該組件的基本結構已經清晰,但還存在一個問題:組件加載時是異步拉取的 options, 而頁面初始化的時候極可能 options 尚未返回,致使 select 的 options 仍爲空。因此此處應該修改代碼,同步獲取 options:

// 策略組修改
source: {
    action: "sourceOption",
    getOptions: async function() {
        // await 拉取 options
    }
  },
// 組件修改
methods: {
    ...
    async valuation() {
        ...
    }
}

繼續優化

但咱們不是每次加載組件都須要拉取 options,若是這些 options 在其餘組件或者頁面也被使用到,那麼能夠考慮將其存入 vuex 中。

最開始的思路是高階組件,即定義一個包裝後的select模板,經過高階組件的方式擴展其數據源與action(變化的部分)然而這個思路不是那麼的vue(主要是slots不太好處理) 因而考慮策略模式改寫該組件

總結

經過以上兩個例子,咱們能夠看到:

  • 策略模式符合開放-封閉原則
  • 若是代碼裏須要寫大量的if-else語句,那麼考慮使用策略模式
  • 若是多個組件(類)之間的區別僅在於它們的行爲,考慮採用策略模式

參考
JavaScript設計模式與開發實踐(曾探) 第五章 策略模式

相關文章
相關標籤/搜索