減小前端代碼耦合

什麼是代碼耦合?代碼耦合的表現是改了一點毛髮而牽動了全身,或者是想要改點東西,須要在一堆代碼裏面找半天。因爲前端須要組織js/css/html,耦合的問題可能會更加明顯,下面按照耦合的狀況分別說明:javascript

這應該是比較常見的耦合。全局耦合就是幾個類、模塊共用了全局變量或者全局數據結構,特別是一個變量跨了幾個文件。例以下面,在html裏面定義了一個變量:css

<script>
    var PAGE = 20;
</script>
 
<script src="main.js"></script>

 

上面在script標籤裏面定義了一個PAGE的全局變量,而後在main.js裏面使用。這樣子PAGE就是一個全局變量,而且跨了兩個文件,一個html,一個js。而後在main.js裏面忽然冒出來了個PAGE的變量,後續維護這個代碼的人看到這個變量處處找不到它的定義,最後找了半天發現原來是在xxx.html的script標籤裏面定義了。這樣就有點egg pain了,而且這樣的變量容易和本地的變量發生命名衝突。html

因此若是須要把數據寫在頁面上的話,一個改進的辦法是在頁面寫一個form,數據寫成form裏面的控件數據,以下:前端

<form id="page-data">
    <input type="hidden" name="page" value="2">
    <textarea name="list" style="display:none">[{"userName": ""yin"},{}]</textarea>
</form>

 

上面使用了input和textarea,使用textarea的優勢是支持特殊符號。再把form的數據序列化,序列化也是比較簡單的,能夠查看Effective前端2:優化html標籤java

第二種是全局數據結構,這種可能會使用模塊化的方法,以下:react

//data.js
module.exports = {
    houseList: null
}
 
//search.js 獲取houseList的數據
var data = require("data");
data.houseList = ajax();
require("format-data").format();
 
//format-data.js 對houseList的數據作格式化
function format(){
    var data = require("data");
    process(data);
    require("show-result").show();
}
 
//show-result.js 將數據顯示出來
function show(){
    showData(require("data").houseList)
}

上面四個模塊各司其職,乍一眼看上去好像沒什麼問題,可是他們都用了一個data的模塊共用數據。這樣確實很方便,可是這樣就全局耦合了。由於用的同一個data,因此你沒法保證,其它人也會加載了這個模塊而後作了些修改,或者是在你的某一個業務的異步回調也改了這個。第二個問題:你不知道這個data是從哪裏來的,誰可能會對它作了修改,這個過程對於後續的模塊來講都是不透明的。webpack

因此這種應該考慮使用傳參的方式,下降耦合度,把data做爲一個參數傳遞:es6

/去掉data.js
//search.js 獲取數據並傳遞給下一個模塊
var houseList = ajax();
require("format-data").format(houseList);
 
//format-data.js 對houseList的數據作格式化
function format(houseList){
    process(houseList);
    require("show-result").show(houseList);
}
 
//show-result.js 將數據顯示出來
function show(houseList){
    showData(houseList)
}

能夠看到,search裏面獲取到data後,交給format-data處理,format-data處理完以後再給show-result。這樣子就很清楚地知道數據的處理流程,而且保證了houseList不會被某個異步回調不當心改了。若是單獨從某個模塊來講,show-result這個模塊並不須要關心houseList的通過了哪些流程和處理,它只須要關心輸入是符合它的格式要求的就能夠。web

這個時候你可能會有一個問題:這個data被逐層傳遞了這麼屢次,還不如像最上面的那樣寫一個data的模塊,你們都去改那裏,豈不是簡單了不少?對,這樣是簡單了,可是一個數據結構被跨了幾個文件使用,這樣會出現我上面說的問題。有時候可能出現一些意想不到的狀況,到時候可能得找bug找個半天。因此這種解耦是值得的,除非你定義的變量並不會跨文件,它的做用域只在它所在的文件,這樣會好不少。或者是data是常量的,data裏面的數據定義好以後值就不再會改變,這樣應當也是可取的。ajax

 

2. js/css/html的耦合

這種耦合在前端裏面應該最多見,由於這三者一般具備交集,須要使用js控制樣式和html結構。若是使用js控制樣式,不少人都喜歡在js裏面寫樣式,例如當頁面滑動到某個地方以後要把某個條吸頂:

很多人會這麼寫:

$(".bar").css({
    position: fixed;
    top: 0;
    left: 0;
});

而後當用戶往上滑的時候取消fixed:

$(".bar").css({
    position: static;
});

若是你用react,你可能會設置一個style的state數據,但其實這都同樣,都把css雜合到js裏面了。某個想要檢查你樣式的人,想要給你改個bug,他檢查瀏覽器發現有個標籤style裏的屬性,而後他找半天找不到是在哪裏設置的,最後他發現是在某個js的某個隱蔽的角落設置了。你在js裏面設置了樣式,而後css裏面也會有樣式,在改css的時候,若是不知道js裏面也有設置了樣式,那麼可能會發生衝突,在某種條件下觸發了js裏面設置樣式。

因此不推薦直接在js裏面更改樣式屬性,而應該經過增刪類來控制樣式,這樣子樣式仍是迴歸到css文件裏面。例如上面能夠改爲這樣:

//增長fixed
$(".bar").addClass("fixed");
 
//取消fixed
$(".bar").removeClass("fixed");

fixed的樣式:

.bar.fixed{
    position: fixed;
    left: 0;
    top: 0;
}

 

能夠看到,這樣的邏輯就很是清晰,而且回滾fixed,不須要把它的position還原爲static,由於它不必定是static,也有多是relative,這種方式在取消掉一個類的時候,不須要去關心本來是什麼,該是什麼就會是什麼。

可是有一種是避免不了的,就是監聽scroll事件或者mousemove事件,動態地改變位置。

這種經過控制類的方式還有一個好處,就是當你給容器動態地增刪一個類時,你能夠藉助子元素選擇器,用這個類控制它的子元素的樣式,也是很方便。

還有不少人可能會以爲html和css/js脫耦,那就是不能在html裏面寫style,不能在html裏面寫script標籤,可是凡事都不是絕對的,若是有一個標籤,它和其它標籤就一個font-size不同,那你直接給它寫一個font-size的內聯樣式,又未嘗不可呢,在性能上來講,若是你寫個class,它還得去匹配這個class,比不上style高效吧。或者是你這個html文件就那麼20、30行css,那直接在head標籤加個style,直接寫在head裏面好了,這樣你就少管理了一個文件,而且瀏覽器不用去加載一個外鏈的文件。

有時候直接在html寫script標籤是必要的,它的優點也是不用加載外鏈文件,處理速度會很快,幾乎和dom渲染同時,這個在解決頁面閃動的時候比較有用。由於若是要用js動態地改變已經加載好的dom,放在外鏈裏面確定會閃一下,而直接寫的script就不會有這個問題,即便這個script是放在了body的後面。例以下面:

原始數據是帶p標籤的,可是在textarea裏面展現的時候須要把p改爲換行\r\n,若是在dom渲染以後再在外鏈裏面更新dom就會出現上面的閃動的狀況。你可能會說我用react,數據都是動態渲染的,渲染前已經處理好了,不會出現上面的狀況。那麼,好吧,至少你瞭解一下吧。

和耦合相對的是內聚,寫代碼的原則就是低耦合、高聚合。所謂內聚就是說一個模塊的職責功能十分緊密,不可分割,這個模塊就是高內聚的。咱們先從重複代碼提及:

3. 減小重複代碼

假設有一段代碼在另一個地方也要被用到,但又不太同樣,那麼最簡單的方法固然是copy一下,而後改一改。這也是很多人採起的辦法,這樣就致使了:若是之後要改一個相同的地方就得同時改好多個地方,就很麻煩了。

例若有一個搜索的界面:

用戶能夠經過點擊search按鈕觸發搜索,也能夠經過點擊下拉或者經過輸入框的change觸發搜索,因此你可能會這麼寫:

$("#search").on("click", function(){
    var formData = getFormData();
    $.ajax({
        url: '/search',
        data: formData,
        success: function(data){
            showResult(data);
        }
    });
});

 

在change裏面又從新發請求:

$("input").on("change", function(){
    //把用戶的搜索條件展現進行改變
    changeInputFilterShow();
    var formData = getFormData();
    $.ajax({
        url: '/search',
        data: formData,
        success: function(data){
            showResult(data);
        }
    });
});

 

change裏面須要對搜索條件的展現進行更改,和click事件不太同樣,因此圖一時之快就把代碼拷了一下。可是這樣是不利於代碼的維護的,因此你可能會想到把獲取數據和發請求的那部分代碼單獨抽離封裝在一個函數,而後兩邊都調一下:

function getAndShowData(){
    var formData = getFormData();
    $.ajax({
        url: '/search',
        data: formData,
        success: function(data){
            showResult(data);
        }
    });
}
 
$("#search").on("click", getAndShowData);
$("input").on("change", function(){
    changeInputFilterShow();
    getAndShowData();
});

 

在抽成一個函數的基礎上,又發現這個函數其實有點大,由於這裏面要獲取表單數據,還要對數據進行格式化,用作請求的參數。若是用戶觸發得比較快,還要記錄上次請求的xhr,在每次發請求前cancle掉上一次的xhr,而且可能對請求作一個loading效果,增長用戶體驗,還要對出錯的狀況進行處理,所有都要在ajax裏面。因此最好對getAndShowData繼續拆分,很天然地會想到把它分離成一個模塊,一個單獨的文件,叫作search-ajax。全部發請求的處理都在這個模塊裏面統一操做。對外只提供一個search.ajax的接口,傳的參數爲當前的頁數便可。全部須要發請求的都調一下這個模塊的這個接口就行了,除了上面的兩種狀況,還有點擊分頁的情景。這樣無論哪一種情景都很方便,我不須要關心請求是怎麼發的,結果是怎麼處理的,我只要傳一個當前的頁數給你就行了。

再往下,會發現,在顯示結果那裏,即上面代碼的第7行,須要對有結果、無結果的狀況分別處理,因此又搞了一個函數叫作showResult,這個函數有點大,它裏面的邏輯也比較複雜,有結果的時候除了更新列表結果,還要更新結果總數、更新分頁的狀態。所以這個showResult一個函數難以擔當大任。因此把這個show-result也當獨分離出一個模塊,負責結果的處理。

到此,咱們整一個search的UML圖應該是這樣的:

注意上面把發請求的又再單獨封裝成了一個模塊,由於這個除了搜索發請求外,其它的請求也能夠用到。同時search-result會用到兩個展現的模板。

因爲不僅一個頁面會用到搜索的功能,因此再把上面繼續抽象,把它封裝成一個search-app的模塊,須要用到的頁面只需require這個search-app,調一下它的init函數,而後傳些定製的參數就能夠用了。這個search-app就至關於一個搜索的插件。

因此整一個的思路是這樣的:出現了重複代碼 -> 封裝成一個函數 -> 封裝成一個模塊 -> 封裝成一個插件,抽象級別不斷提升,將共有的特性和有差別的地方分離出來。當你走在抽象與封裝的路上的時候,那你應該也是走在了大神的路上。

固然,若是兩個東西並無共同點,可是你硬是要搞在一塊兒,那是不可取的。

我這裏說的封裝並非說,你必定要使用requirejs、es6的import或者是webpack的require,關鍵在於你要有這種模塊化的思想,並非指工具上的,無論你用的哪個,只要你有這種抽象的想法,那都是可取的。

模塊化的極端是拆分粒度太細,一個簡單的功能,明明十行代碼寫在一塊兒就能夠搞定的事情,硬是寫了7、八層函數棧,每一個函數只有兩、三行。這樣除了把你的邏輯搞得太複雜以外,並無太多的好處。當你出現了重複代碼,或者是一個函數太大、功能太多,又或是邏輯裏面寫了三層循環又再嵌套了三層if,再或是你預感到你寫的這個東西其餘人也可能會用到,這個時候你才考慮模塊化,進行拆分比較合適。

上面無論是search-result仍是search-ajax他們在功能上都是高度內聚的,每一個模塊都有本身的職責,不可拆分,這在面向對象編程裏面叫作單一責職原則,一個模塊只負責一個功能。

再舉一個例子,我在怎樣實現前端裁剪上傳圖片功能裏面提到一個上傳裁剪的實現,這裏麪包含裁剪、壓縮上傳、進度條三大功能,因此我把它拆成三個模塊:

這裏提到的模塊大部分是一個單例的object,不會去實例它,通常能夠知足大部分的需求。在這個單例的模塊裏面,它本身的「私有」函數通常是經過傳參調用,可是若是須要傳遞的數據比較多的時候,就有點麻煩了,這個時候能夠考慮把它封裝成一個類。

3. 封裝成一個類

在上面的裁剪上傳裏面的進度條progress-bar,一個頁面裏可能有幾個要上傳的地方,每一個上傳的地方都會有進度條,每一個進度條都有本身的數據,因此不能像在最上面說的,在一個文件的最上面定義一些變量而後爲這個模塊裏面的函數共用,只能是經過傳遞參數的形式,即在最開始調用的時候定義一些數據,而後一層一層地傳遞下去。若是這些數據不少的話就有點麻煩。

因此稍微變通一下,把progress-bar封裝成一個類:

function ProgressBar($container){
    this.$container = $container; //進度條外面的容器
    this.$meter = null;           //進度條可視部分
    this.$bar = null;             //進度條存放可視部分的容器
    this.$barFullWidth = $container.width() * 0.9; //進度條的寬度
    this.show();                  //new一個對象的時候就顯示
}

 

或者你用ES6的class,可是本質上是同樣的,而後這個ProgressBar的成員函數就可使用定義的這些「私有」變量,例如設置進度條的進度函數:

ProgressBar.prototype.setProgress = function(percentage, time){
    time = typeof time === "undefined" ? 100 : time;
    this.$meter.stop().animate({width: parseInt(this.$barFullWidth * percentage)}, time);
};

 

這個使用了兩個私有變量,若是再加上原先兩個,用傳參的方式就得傳四個。

使用類是模塊化的一種思想,另一種經常使用的還有策略模式。

4. 使用策略模式

假設要實現下面三個彈框:

這三個彈框不管是在樣式上仍是在功能上都是同樣的,惟一的區別是上面標題文案是不同的。最簡單的多是把每一個彈框的html都copy一下,而後改一改。若是你用react,你可能會用拆分組件的方式,上面一個組件,下面一個組件,那麼好吧,你就這樣搞吧。若是你沒用react,你可能得想辦法組織下你的代碼。

若是你有策略模式的思想,你可能會想到把上面的標題看成一個個的策略。首先定義不一樣彈框的類型,一一標誌不一樣的彈框:

var popType = ["register", "favHouse", "saveSearch"];

  

定義三種popType一一對應上面的三個彈框,而後每種popType都有對應的文案:

Data.text.pop = {
    register: {
        titlte: "Create Your Free Account",
        subTitle: "Search Homes and Exclusive Property Listings"
    },
    favHouse: {title: "xxx", subTitle: "xxx" },
    saveSearch: {title: "xxx", subTitle: "xxx"}
};

 

{tittle: 「」, subtitle: 「」}這個就看成是彈框文案策略,而後再寫彈框的html模板的時候引入一個佔位變量:

<section>
    {{title}}
    {{subTitile}}
    <div>
        <!--其它內容-->
    </div>
</section>

 

在渲染這個彈框的時候,根據傳進來的popType映射到不一樣的文案:

function showPop(popType){
    Mustache.render(popTemplate, Data.text.pop[popType])
}

 

這裏用Data.text.pop[popType]映射到了對應的文案,若是用react你把一個個的標題封裝成一個組件,其實思想是同樣的。

可是這個並非嚴格的策略模式,由於策略就是要有執行的東西嘛,咱們這裏實際上是一個寫死的文案,可是咱們藉助了策略模式的思想。接下來繼續說使用策略模式作一些執行的事情。

在上面的彈框的觸發機制分別是:用戶點擊了註冊、點擊了收藏房源、點擊了保存搜索條件。若是用戶沒有登錄就會彈一個註冊框,當用戶註冊完以後,要繼續執行用戶本來的操做,例如該收藏仍是收藏,因此必需要有一個註冊後的回調,而且這個回調作的事情還不同。

固然,你能夠在回調裏面寫不少的if else或者是case:

function popCallback(popType){
    switch(popType){
        case "register": 
            //do nothing
            break;
        case: "favHouse": 
            favHouse();
            break;
        case: "saveSearch":
            saveSearch();
            break;
    }
}

 

可是當你的case不少的時候,看起來可能就不是特別好了,特別是if else的那種寫法。這個時候就可使用策略模式,每一個回調都是一個策略:

var popCallback = {
    favHouse: function(){
        //do sth.
    },
    saveSearch: function(){
        //do sth.
    }
}

 

而後根據popType映射調用相應的callback,以下:

var popCallback = require("pop-callback");
if(typeof popCallback[popType] === "function"){
    popCallback[popType]();
}

 

這樣它就是一個完整的策略模式了,這樣寫有不少好處。若是之後須要增長一個彈框類型popType,那麼只要在popCallback裏面添加一個函數就行了,或者要刪掉一個popType,相應地註釋掉某個函數便可。並不須要去改動原有代碼的邏輯,而採用if else的方式就得去修改原有代碼的邏輯,因此這樣對擴展是開放的,而對修改是封閉的,這就是面向對象編程裏面的開閉原則。

在js裏面實現策略模式或者是其它設計模式都是很天然的方式,由於js裏面function能夠直接做爲一個普通的變量,而在C++/Java裏面須要用一些技巧,玩一些OO的把戲才能實現。例如上面的策略模式,在Java裏面須要先寫一個接口類,裏面定義一個接口函數,而後每一個策略都封裝成一個類,分別實現接口類的接口函數。而在js裏面的設計模式每每幾行代碼就寫出來,這可能也是作爲函數式編程的一個優勢。

前端和設計模式常常打交道的還有訪問者模式

4. 訪問者模式

事件監聽就是一個訪問者模式,一個典型的訪問者模式能夠這麼實現,首先定義一個Input的類,初始化它的訪問者列表

function Input(inputDOM){
    //用來存放訪問者的數據結構
    this.visitiors = {
        "click": [],
        "change": [],
        "special": [] //自定義事件
    }
    this.inputDOM = inputDOM;
}

 

而後提供一個對外的添加訪問者的接口:

Input.prototype.on = function(eventType, callback){
    if(typeof this.visitiors[eventType] !== "undefined"){
        this.visitiors[eventType].push(callback);
    }
};

 

使用者調用on,傳遞兩個參數, 一個是事件類型,即訪問類型,另一個是具體的訪問者,這裏是回調函數。Input就會將訪問者添加到它的訪問者列表。

同時Input還提供了一個刪除訪問者的接口:

Input.prototype.off = function(eventType, callback){
    var visitors = this.visitiors[eventType];
    if(typeof visitiors !== "undefined"){
        var index = visitiors.indexOf(callback);
        if(index >= 0){
            visitiors.splice(index, 1);
        }
    }
};

 

這樣子,Input就和訪問者創建起了關係,或者說訪問者已經成功地向接收者都訂閱了消息,一旦接書者收到了消息會向它的訪問者一一傳遞:

Input.prototype.trigger = function(eventType, event){
    var visitors = this.visitiors[eventType];
    var eventFormat = processEvent(event); //獲取消息並作格式化
    if(typeof visitors !== "undefined"){
        for(var i = 0; i < visitors.length; i++){
            visitors[i](eventFormat);
        }
    }
};

 

trigger多是用戶調的,也多是底層的控件調用的。在其它領域,它多是一個光感控件觸發的。無論怎樣,一旦有人觸發了trigger,接收者就會一一下發消息。

若是你知道了事件監聽的模式是這樣的,可能對你寫代碼會有幫助。例如點擊下面的搜索條件的X,要把上面的搜索框清空,同時還要觸發搜索,並把輸入框右邊的X去掉。要附帶着作幾件事情。

這個時候你可能會這樣寫:

$(".icon-close").on("click", function(){
    $(this).parent().remove(); //刪除自己的展現
    $("#search-input").val("");
    searchAjax.ajax();         //觸發搜索
    $("#clear-search").hide(); //隱藏輸入框x
});

 

但其實這樣有點累贅,由於在上面的搜索輸入框確定也會相應的操做,當用戶輸入爲空時,自動隱藏右邊的x,而且輸入框change的時候會自動搜索,也就是說全部附加的事情輸入框那邊已經有了,因此其實只須要觸發下輸入框的change事件就行了:

$(".icon-close").on("click", function(){
    $(this).parent().remove(); //刪除自己的展現
    $("#search-input").val("").trigger("change");
});

 

輸入框爲空時,該怎麼處理,search輸入框會相應地處理,下面那個條件展現的x不須要去關心。觸發了change以後,會把相應的消息下發給search輸入框的訪問者們。

固然,你用react你可能不會這樣想了,你應該是在研究組件間怎麼通訊地好。

上文說起使用傳參避免全局耦合,而後在js裏面經過控制class減小和css的耦合,和耦合相對的是內聚,出發點是重複代碼,減小拷貝代碼會有一個抽象和封裝的過程:function -> 模塊 -> 插件/框架,封裝經常使用的還有封裝成一個類,方便控制私有數據。這樣可實現高內聚,除此方法,還有設計模式的思想,上面介紹了策略模式和訪問者模式的原理和應用,以及在寫代碼的啓示。

相關文章
相關標籤/搜索