减小前端代码耦合

什么是代码耦合?代码耦合的表现是改了一点毛发而牵动了全身,或者是想要改点东西,须要在一堆代码里面找半天。因为前端须要组织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 -> 模块 -> 插件/框架,封装经常使用的还有封装成一个类,方便控制私有数据。这样可实现高内聚,除此方法,还有设计模式的思想,上面介绍了策略模式和访问者模式的原理和应用,以及在写代码的启示。

相关文章
相关标签/搜索