什么是代码耦合?代码耦合的表现是改了一点毛发而牵动了全身,或是想要改点东西,必须在一堆代码里面找半天。因为前端必须组织js/css/html,耦合的问题可能会更加显著,下面根据耦合的状况分别说明:
1. 避免全局耦合
这应该是比较平常的耦合。全局耦合便是几个类、模块共用了全局变量或全局数据结构,尤其是一个变量跨了几个文件。例如下面,在html里面定义了一个变量:
var PAGE = 20
; </script
> <script src="main.js"
> </script>
上面在head标签里面定义了一个PAGE的全局变量,而后在main.js里面运用。这般子PAGE便是一个全局变量,并且跨了两个文件,一个html,一个js。而后在main.js里面忽然冒出来了个PAGE的变量,后续守护这个代码的人看到这个变量到处找不到它的定义,最后找了半天发掘原来是在xxx.html的head标签里面定义了。这般就有点egg pain了,并且这般的变量容易和本地的变量出现命名冲突。
因此倘若必须把数据写在页面上的话,一个改进的办法是在页面写一个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标签(http://yincheng.site/make-the-best-of-html-label)
第二种是全局数据结构,这种可能会运用模块化的办法,如下: //data.jsmodule
.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是从哪里来的,谁可能会对它做了修改,这个过程针对后续的模块来讲都是不透明的。
因此这种应该思虑运用传参的方式,降低耦合度,把data做为一个参数传递: //去掉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的经过了那些流程和处理,它只必须关心输入是符合它的格式需求的就能够。
这个时候你可能会有一个问题:这个data被逐层传递了这么多次,还不如像最上面的那样写一个data的模块,大众都去改那里,岂不是简单了非常多?对,这般是简单了,然则一个数据结构被跨了几个文件运用,这般会显现我上面说的问题。有时候可能显现有些意想不到的状况,到时候可能得找bug找个半天。因此这种解耦是值得的,除非你定义的变量并不会跨文件,它的功效域仅在它所在的文件,这般会好非常多。或是data是常量的,data里面的数据定义好之后值就再亦不会改变,这般应当亦是可取的。
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她们在功能上都是高度内聚的,每一个模块都有自己的职责,不可拆分,这在面向对象编程里面叫做单一责职原则,一个模块只负责一个功能。
再举一个例子,我在怎么样实现前端裁剪上传照片功能(http://yincheng.site/crop-upload-photo)里面说到一个上传裁剪的实现,这儿面包括裁剪、压缩上传、进度条三大功能,因此我把它拆成三个模块:
这儿说到的模块大部分是一个单例的object,不会去实例它,通常能够满足大部分的需求。在这个单例的模块里面,它自己的“私有”函数通常是经过传参调用,然则倘若必须传递的数据比较多的时候,就有点麻烦了,这个时候能够思虑把它封装成一个类。
4. 封装成一个类
在上面的裁剪上传里面的进度条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); };
这个运用了两个私有变量,倘若再加上原先两个,用传参的方式就得传四个。
运用类是模块化的一种思想,另一一种常用的还有策略模式。
5. 运用策略模式
假设要实现下面三个弹框:
这三个弹框没论是在样式上还是在功能上都是同样的,独一的区别是上面标题文案是不同样的。最简单的可能是把每一个弹框的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模板的时候引入一个占位变量:
{{title}}
{{subTitile}} <!--其它内容--> </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里面的设计模式常常几行代码就写出来,这可能亦是做为函数式编程的一个优点。
前端和设计模式经常打交道的还有拜访者模式。
6. 拜访者模式
事件监听便是一个拜访者模式,一个典型的拜访者模式能够这么实现,首要定义一个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)
{ varvisitors =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 -> 模块 -> 插件/框架,封装常用的还有封装成一个类,方便掌控私有数据。这般可实现高内聚,除此办法,还有设计模式的思想,上面介绍了策略模式和拜访者模式的原理和应用,以及在写代码的启示。
期盼上文能对你有所启迪,如有不对之处还请指出。
—— 上期头条 ——
前端工程师必须实用网站
『极乐科技』专栏原创作者征集进行时…
(点击阅读原文直达极乐知乎专栏)
倘若你乐分享、愿交流、钻技术;
倘若你期盼记录自己成长过程;
倘若你期待与大牛比肩;
倘若你巴望得到更加多关注…
欢迎加入咱们知乎专栏原创作者团队
咱们原创作者团队有大牛,有妹纸,有逗比!
咱们原创作者团队能交流,善协作,爱学习!
加入原创作者团队,结实更加多的他他她!
另外,针对优秀原创内容,极乐科技会予以关联激励!让劳有所得!
倘若你对此有兴趣,欢迎在微X平台留言!
在阅读文案过程中如有疑问,请发布到极乐官网;或长按下方二维码,关注极官方微X平台,在平台下方留言,咱们将第1时间为您解答。
|