原文收錄在個人 GitHub博客 (https://github.com/jawil/blog) ,喜歡的能夠關注最新動態,你們一塊兒多交流學習,共同進步,以學習者的身份寫博客,記錄點滴。html
在一個
Web
項目中,註冊,登陸,修改用戶信息,下訂單等功能的實現都離不開提交表單。這篇文章就闡述瞭如何編寫相對看着舒服的表單驗證代碼。前端
假設咱們正在編寫一個註冊的頁面,在點擊註冊按鈕以前,有以下幾條校驗邏輯。git
全部選項不能爲空es6
用戶名長度不能少於6位github
密碼長度不能少於6位算法
手機號碼必須符合格式編程
郵箱地址必須符合格式設計模式
注:爲簡單起見,如下例子以傳統的瀏覽器表單驗證,Ajax
異步請求不作探討,瀏覽器端驗證原理圖:數組
簡要說明:瀏覽器
這裏咱們前端只作瀏覽器端的校驗。不少工具能夠在表單檢驗事後、瀏覽器發送請求前截取表單數據,攻擊者能夠修改請求中的數據,從而繞過
JavaScript
,將惡意數據注入服務器,這樣會增長XSS(全稱 Cross Site Scripting)攻擊的機率。對於通常的網站,都不同意採用瀏覽器端的表單驗證方法。瀏覽器端和服務器端雙重驗證方法在瀏覽器端驗證方法基礎上增長服務器端的驗證,其原理如圖所示,該方法增長服務器端的驗證,彌補了傳統瀏覽器端驗證的缺點。若表單輸入不符合要求,瀏覽器端的Javascript
驗證能很快地給出響應,而服務器端的驗證則能夠防止惡意用戶繞過Javascript
驗證,保證最終數據的準確性。
HTML代碼:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>探索幾種表單驗證最佳實踐方式</title> </head> <body> <form action="http://xxx.com/register" id="registerForm" method="post"> <div class="form-group"> <label for="user">請輸入用戶名:</label> <input type="text" class="form-control" id="user" name="userName"> </div> <div class="form-group"> <label for="pwd">請輸入密碼:</label> <input type="password" class="form-control" id="pwd" name="passWord"> </div> <div class="form-group"> <label for="phone">請輸入手機號碼:</label> <input type="tel" class="form-control" id="phone" name="phoneNumber"> </div> <div class="form-group"> <label for="email">請輸入郵箱:</label> <input type="text" class="form-control" id="email" name="emailAddress"> </div> <button type="button" class="btn btn-default">Submit</button> </form> </body> </html>
JavaScript代碼:
let registerForm = document.querySelector('#registerForm') registerForm.addEventListener('submit', function() { if (registerForm.userName.value === '') { alert('用戶名不能爲空!') return false } if (registerForm.userName.length < 6) { alert('用戶名長度不能少於6位!') return false } if (registerForm.passWord.value === '') { alert('密碼不能爲空!') return false } if (registerForm.passWord.value.length < 6) { alert('密碼長度不能少於6位!') return false } if (registerForm.phoneNumber.value === '') { alert('手機號碼不能爲空!') return false } if (!/^1(3|5|7|8|9)[0-9]{9}$/.test(registerForm.phoneNumber.value)) { alert('手機號碼格式不正確!') return false } if (registerForm.emailAddress.value === '') { alert('郵箱地址不能爲空!') return false } if (!/^\w+([+-.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)* $/.test(registerForm.emailAddress.value)) { alert('郵箱地址格式不正確!') return false } }, false)
這樣編寫代碼,的確可以完成業務的需求,可以完成表單的驗證,可是存在不少問題,好比:
registerForm.addEventListener
綁定的函數比較龐大,包含了不少的if-else
語句,看着都噁心,這些語句須要覆蓋全部的校驗規則。
registerForm.addEventListener
綁定的函數缺少彈性,若是增長了一種新的校驗規則,或者想要把密碼的長度校驗從6改爲8,咱們都必須深刻registerForm.addEventListener
綁定的函數的內部實現,這是違反了開放-封閉原則的。算法的複用性差,若是程序中增長了另外一個表單,這個表單也須要進行一些相似的校驗,那咱們極可能將這些校驗邏輯複製得漫天遍野。
所謂辦法總比問題多,辦法是有的,好比立刻要講解的使用 策略模式 使表單驗證更優雅更完美,我相信不少人很抵觸設計模式,一聽設計模式就以爲很遙遠,以爲本身在工做中不多用到設計模式,那麼你就錯了,特別是JavaScript
這種靈活的語言,有的時候你已經在你的代碼中使用了設計模式,只是你不知道而已。更多關於設計模式的東西,之後會陸續寫博客描述,這裏只但願你們拋棄設計模式神祕的感受,通俗的講,它無非就是完成一件事情通用的辦法而已。
回到正題,假如咱們不想使用過多的 if - else
語句,那麼咱們心中比較理想的代碼編寫方式是什麼呢?咱們能不能像編寫配置同樣的去作表單驗證呢?再來一個」一鍵驗證「的功能,是否是很爽?答案是確定的,因此咱們心中理想的編寫代碼的方式以下:
// 獲取表單form元素 let registerForm = document.querySelector('#registerForm') // 建立表單校驗實例 let validator = new Validator(); // 編寫校驗配置 validator.add(registerForm.userName, 'isNonEmpty', '用戶名不能爲空') validator.add(registerForm.userName, 'minLength:6', '用戶名長度不能小於6') // 開始校驗,並接收錯誤信息 let errorMsg = validator.start() // 若是有錯誤信息輸出,說明校驗未經過 if(errorMsg){ alert(errorMsg) return false//阻止表單提交 }
怎麼樣?感覺感覺,是否是看上去優雅多了?好了,有了這些思路,咱們就能夠向目標邁進了,下一步就要了解了解什麼事策略模式了。
策略模式,單純的看它的名字」策略「,指的是作事情的方法,好比咱們想到某個地方旅遊,你能夠有幾種策略供選擇:
一、飛機,嗖嗖嗖直接就到了,節省時間。
二、火車,能夠選擇高鐵出行,專爲飛機恐懼症者提供。
三、徒步,不失爲一個鍛鍊身體的選擇。
四、other method……
在程序設計中,咱們也常常遇到相似的狀況,要實現一種方案有多種方案能夠選擇,好比,一個壓縮文件的程序,便可選擇zip算法,也能夠選擇gzip算法。
因此,作一件事你會有不少方法,也就是所謂的策略,而咱們今天要講的策略模式也就是這個意思,它的核心思想是,將作什麼和誰去作相分離。因此,一個完整的策略模式要有兩個類,一個是策略類,一個是環境類(主要類),環境類接收請求,但不處理請求,它會把請求委託給策略類,讓策略類去處理,而策略類的擴展是很容易的,這樣,使得咱們的代碼易於擴展。
在表單驗證的例子中,各類驗證的方法組成了策略類,好比:判斷是否爲空的方法(如:isNonEmpty),判斷最小長度的方法(如:minLength),判斷是否爲手機號的方法(isMoblie)等等,他們組成了策略類,供給環境類去委託請求。下面,咱們就來實戰一下。
策略模式的組成
抽象策略角色:策略類,一般由一個接口或者抽象類實現。
具體策略角色:包裝了相關的算法和行爲。
環境角色:持有一個策略類的引用,最終給客戶端用的。
策略類很簡單,它是由一組驗證方法組成的對象,即策略對象,重構表單校驗的代碼,很顯然第一步咱們要把這些校驗邏輯都封裝成策略對象:
/*策略對象*/ const strategies = { isNonEmpty(value, errorMsg) { return value === '' ? errorMsg : void 0 }, minLength(value, length, errorMsg) { return value.length < length ? errorMsg : void 0 }, isMoblie(value, errorMsg) { return !/^1(3|5|7|8|9)[0-9]{9}$/.test(value) ? errorMsg : void 0 }, isEmail(value, errorMsg) { return !/^\w+([+-.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/.test(value) ? errorMsg : void 0 } }
根據咱們的思考,咱們使用add
方法添加驗證配置,以下:
validator.add(registerForm.userName, 'isNonEmpty', '用戶名不能爲空') validator.add(registerForm.userName, 'minLength:6', '用戶名長度不能小於6')
add
方法接受三個參數,第一個參數是表單字段,第二個參數是策略對象中策略方法的名字,第三個參數是驗證未經過的錯誤信息。
而後使用 start
方法開始驗證,若驗證未經過,返回驗證錯誤信息,以下:
let errorMsg = validator.start()
另外,再解釋一下下面這句代碼:
add
方法第一個參數咱們說過了,是要驗證的表單元素,第二個參數是一個字符串,使用 冒號(:) 分割,前面是策略方法名稱,後面是傳給這個方法的參數,第三個參數仍然是錯誤信息。
可是這種參數配置仍是有問題,咱們的要求是多種校驗規則,好比用戶名既不能爲空,又要知足用戶名長度不小於6,並非單一的,上面的爲何要寫兩次,這種看着就不舒服,這時候我就須要對配置參數作一點小小的改動,咱們用數組來傳遞多個校驗規則:
validator.add(registerForm.userName, [{ strategy: 'isNonEmpty', errorMsg: '用戶名不能爲空!' }, { strategy: 'minLength:6', errorMsg: '用戶名長度不能小於6位!' }])
最後是Validator類的實現:
/*Validator類*/ class Validator { constructor() { this.cache = [] //保存校驗規則 } add(dom, rules) { for (let rule of rules) { let strategyAry = rule.strategy.split(':') //例如['minLength',6] let errorMsg = rule.errorMsg //'用戶名不能爲空' this.cache.push(() => { let strategy = strategyAry.shift() //用戶挑選的strategy strategyAry.unshift(dom.value) //把input的value添加進參數列表 strategyAry.push(errorMsg) //把errorMsg添加進參數列表,[dom.value,6,errorMsg] return strategies[strategy].apply(dom, strategyAry) }) } } start() { for (let validatorFunc of this.cache) { let errorMsg = validatorFunc()//開始校驗,並取得校驗後的返回信息 if (errorMsg) {//r若是有確切返回值,說明校驗沒有經過 return errorMsg } } } }
使用策略模式重構代碼之後,咱們僅僅經過‘配置’的方式就能夠完成一個表單的校驗,這些校驗規則也能夠複用在程序的任何地方,還能做爲插件的形式,方便地被移植到其餘項目中。
/*客戶端調用代碼*/ let registerForm = document.querySelector('#registerForm') const validatorFunc = () => { let validator = new Validator() validator.add(registerForm.userName, [{ strategy: 'isNonEmpty', errorMsg: '用戶名不能爲空!' }, { strategy: 'minLength:6', errorMsg: '用戶名長度不能小於6位!' }]) validator.add(registerForm.passWord, [{ strategy: 'isNonEmpty', errorMsg: '密碼不能爲空!' }, { strategy: 'minLength:', errorMsg: '密碼長度不能小於6位!' }]) validator.add(registerForm.phoneNumber, [{ strategy: 'isNonEmpty', errorMsg: '手機號碼不能爲空!' }, { strategy: 'isMoblie', errorMsg: '手機號碼格式不正確!' }]) validator.add(registerForm.emailAddress, [{ strategy: 'isNonEmpty', errorMsg: '郵箱地址不能爲空!' }, { strategy: 'isEmail', errorMsg: '郵箱地址格式不正確!' }]) let errorMsg = validator.start() return errorMsg } registerForm.addEventListener('submit', function() { let errorMsg = validatorFunc() if (errorMsg) { alert(errorMsg) return false } }, false)
在修改某個校驗規則的時候,只須要編寫或者改寫少許的代碼。好比咱們想要將用戶名輸入框的校驗規則改爲用戶名不能少於4個字符。能夠看到,這時候的修改是絕不費力的。代碼以下:
validator.add(registerForm.userName, [{ strategy: 'isNonEmpty', errorMsg: '用戶名不能爲空!' }, { strategy: 'minLength:4', errorMsg: '用戶名長度不能小於4位!' }])
策略模式利用組合、委託和多態等技術思想,能夠有效的避免多種條件選擇語句;
策略模式提供了對開放-封閉原則的完美支持,將算法封裝在獨立的strategy中,使得它易於切換,易於理解,易於拓展;
策略模式中的算法也能夠複用在系統的其它地方,從而避免了許多重複的複製黏貼的工做;
在策略模式利用組合和委託來讓Context擁有執行算法的能力,這也是繼承一種更輕便的替代方案。
固然,策略模式也有一些缺點,但掌握了策略模式,這些缺點並不嚴重。
編寫難度加大,代碼量變多了,這是最直觀的一個缺點,也算不上缺點,畢竟不能徹底以代碼多少來衡量優劣。
首先,使用策略模式會在程序中增長許多策略類或者策略對象,但實際上這比把它們負責的邏輯堆砌在Context中要好。
其次,要使用策略模式,必須瞭解全部的strategy,必須瞭解各個strategy之間的不一樣點,這樣才能選擇一個合適的strategy。好比,咱們要選擇一種合適的旅遊出行路線,必須先了解選擇飛機、火車、自行車等方案的細節。此時strategy要向客戶暴露它的全部實現,這是違反最少知識原則的。
策略模式使開發人員可以開發出由許多可替換的部分組成的軟件,而且各個部分之間是弱鏈接的關係。
弱鏈接的特性使軟件具備更強的可擴展性,易於維護;更重要的是,它大大提升了軟件的可重用性。
策略模式當然可行,可是包裝的有點多了,並且不便於書寫,代碼書寫量增長了很多,也就是有必定門檻,那有沒有更好的實現方式呢?咱們能不能經過一層代理,在設置屬性時候就去攔截它呢?這就是今天要講到的ES6的Proxy對象。
Proxy 用於修改某些操做的默認行爲,等同於在語言層面作出修改,因此屬於一種「元編程」(meta programming),即對編程語言進行編程。
Proxy 能夠理解成,在目標對象以前架設一層「攔截」,外界對該對象的訪問,都必須先經過這層攔截,所以提供了一種機制,能夠對外界的訪問進行過濾和改寫。Proxy 這個詞的原意是代理,用在這裏表示由它來「代理」某些操做,能夠譯爲「代理器」。
let obj = new Proxy({}, { get (target, key, receiver) { console.log(`getting ${key}!`) return Reflect.get(target, key, receiver) }, set (target, key, value, receiver) { console.log(`setting ${key}!`) return Reflect.set(target, key, value, receiver) } })
上面代碼對一個空對象架設了一層攔截,重定義了屬性的讀取(get
)和設置(set
)行爲。這裏暫時先不解釋具體的語法,只看運行結果。對設置了攔截行爲的對象obj
,去讀寫它的屬性,就會獲得下面的結果。
obj.count = 1 // setting count! ++obj.count // getting count! // setting count! // 2
上面代碼說明,Proxy 實際上重載(overload)了點運算符,即用本身的定義覆蓋了語言的原始定義。
ES6 原生提供 Proxy 構造函數,用來生成 Proxy 實例。
let proxy = new Proxy(target, handler);
Proxy 對象的全部用法,都是上面這種形式,不一樣的只是handler
參數的寫法。其中,new Proxy()
表示生成一個Proxy
實例,target
參數表示所要攔截的目標對象,handler
參數也是一個對象,用來定製攔截行爲。
下面是另外一個攔截讀取屬性行爲的例子。
var proxy = new Proxy({}, { get: function(target, property) { return 35; } }); proxy.time // 35 proxy.name // 35 proxy.title // 35
上面代碼中,做爲構造函數,Proxy
接受兩個參數。第一個參數是所要代理的目標對象(上例是一個空對象),即若是沒有Proxy
的介入,操做原來要訪問的就是這個對象;第二個參數是一個配置對象,對於每個被代理的操做,須要提供一個對應的處理函數,該函數將攔截對應的操做。好比,上面代碼中,配置對象有一個get
方法,用來攔截對目標對象屬性的訪問請求。get
方法的兩個參數分別是目標對象和所要訪問的屬性。能夠看到,因爲攔截函數老是返回35
,因此訪問任何屬性都獲得35
。
注意,要使得Proxy
起做用,必須針對Proxy
實例(上例是proxy
對象)進行操做,而不是針對目標對象(上例是空對象)進行操做。
利用proxy攔截不符合要求的數據
function validator(target, validator, errorMsg) { return new Proxy(target, { _validator: validator, set(target, key, value, proxy) { let errMsg = errorMsg if (value == '') { alert(`${errMsg[key]}不能爲空!`) return target[key] = false } let va = this._validator[key] if (!!va(value)) { return Reflect.set(target, key, value, proxy) } else { alert(`${errMsg[key]}格式不正確`) return target[key] = false } } }) }
負責校驗的邏輯代碼
const validators = { name(value) { return value.length > 6 }, passwd(value) { return value.length > 6 }, moblie(value) { return /^1(3|5|7|8|9)[0-9]{9}$/.test(value) }, email(value) { return /^\w+([+-.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/.test(value) } }
客戶端調用代碼
const errorMsg = { name: '用戶名', passwd: '密碼', moblie: '手機號碼', email: '郵箱地址' } const vali = validator({}, validators, errorMsg) let registerForm = document.querySelector('#registerForm') registerForm.addEventListener('submit', function() { let validatorNext = function*() { yield vali.name = registerForm.userName.value yield vali.passwd = registerForm.passWord.value yield vali.moblie = registerForm.phoneNumber.value yield vali.email = registerForm.emailAddress.value } let validator = validatorNext() validator.next(); !vali.name || validator.next(); //上一步的校驗經過才執行下一步 !vali.passwd || validator.next(); !vali.moblie || validator.next(); }, false)
優勢:條件和對象自己徹底隔離開,後續代碼的維護,代碼整潔度,以及代碼健壯性和複用性變得很是強。
缺點:兼容性很差,有babel怕啥,粗糙版,不少細節其實還能夠優化,這裏只提供一種思路。