1.前言
上篇文章,主要介紹了重構的一些概念和一些簡單的例項。這一次,詳細的說下專案中的一個重構場景--給API設計擴充套件機制。目的就是為了方便以後能靈活應對需求的改變。當然了,是否需要設計擴充套件性這個要看API的需求。如果大家有什麼建議,歡迎評論留言。
2.擴充套件性表現形式
2-1.prototype
這個可以說是JS裡面最原的一個擴充套件。比如原生JS沒有提供打亂陣列順序的API,但是開發者又想方便使用,這樣的話,就只能擴充套件陣列的prototype。程式碼如下
//擴充套件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);複製程式碼
執行結果
功能是實現了。但是上面的程式碼,只想借用例子講解擴充套件性,大家看下就好。不要模仿,也不要在專案這樣寫。現在基本都禁止這樣開發了。理由也很簡單,之前的文章也有提到過。這裡重複一下。
這樣就汙染了原生物件Array,別人建立的Array也會被汙染,造成不必要的開銷。最可怕的是,萬一自己命名的跟原生的方法重名了,就被覆蓋原來的方法了。
Array.prototype.push=function(){console.log('守候')}
let arrTest=[123]
arrTest.push()
//result:守候
//push方法有什麼作用,大家應該知道,不知道的可以去w3c看下複製程式碼
2-2.jQuery
關於 jQuery 的擴充套件性,分別提供了三個API:$.extend()、$.fn和$.fn.extend()。分別對jQuery的本身,靜態方法,原型物件進行擴充套件,基於jQuery寫外掛的時候,最離不開的應該就是$.fn.extend()。
參考連結:
理解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方法。如下
使用元件
上面幾個擴充套件性的例項分別是原生物件,庫,框架的擴充套件,大家可能覺得有點誇誇而談,那下面就分享一個日常開發常用的一個例項。
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新增日期的校驗的規則。
至於第三個問題,這樣的想法,可能不算太優雅,呼叫也不是太方便,但是就我現在能想到的,這個就是最好方案啊了。
這個看似是已經做完了,但是大家可能覺得有一種情況沒能應對,比如下面這種,做不到。
因為上面的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 增加擴充套件性。這個例子比較簡單,不算難。大家用這個程式碼在瀏覽器上執行,就很好理解。如果大家對這個例子有什麼更好的建議,或者程式碼上有什麼問題,歡迎在評論區留言,大家多交流,相互學習。
-------------------------華麗的分割線--------------------
想了解更多,關注關注我的微信公眾號:守候書閣