第五章 UI层松耦合

  • A+
所属分类:Web前端
摘要

当你能够做到修改一个组件而不需要去更改其他组件时,你就做到了松耦合。对于多人大型系统来说,很多人参与维护代码,松耦合对于代码可维护性来说至关重要。你绝对希望开发人员在修改某部分代码时不会破环其他人的代码。


UI层松耦合

当你能够做到修改一个组件而不需要去更改其他组件时,你就做到了松耦合。对于多人大型系统来说,很多人参与维护代码,松耦合对于代码可维护性来说至关重要。你绝对希望开发人员在修改某部分代码时不会破环其他人的代码。

当一个大系统的每个组件的内容有了限制,就做到了松耦合。本质上讲,每个组件需要保持足够廋身来确保松耦合。组件知道的越少,就越有利于形成整个系统。

有一点需要注意:在一起工作的组件无法达到“无偶合”。在所有系统中,组件之间总要共享一些信息来完成各自的工作,这很好理解,我们的目标是确保对一个组件的修改不会经常性地影响其他部分。

如果一个Web UI是松耦合的,则很容易测试。和文本或者结构相关问题,通过查找HTML即可定位。当发生了样式相关的问题你就知道出现在CSS中。最后,对于那些行为相关的问题,你直接去JavaScript中找问题所在,这种能力是Web界面的可维护性的核心部分

将JavaScript从CSS中抽离

这个内容比较老旧了,但是还是提一下,IE8和更早的浏览器中有一个让人爱少恨多的 功能,即CSS表达式。CSS表达式允许你将JavaScript直接插入到CSS中,这样可以在CSS代码中直接执行运算或其他操作,不过现在浏览器淘汰了这个功能,作者在这个位置踩过很深的坑,有兴趣可以了解一下哦!!

将CSS从JavaScript中抽离

有时候,保持CSS和JavaScript之间清晰的分离是很有挑战的,这两门语言相互协作得很不错,所以我们常常将样式数据和JavaScript混在一起写。通过脚本修改样式最流行的一种方法是,直接修改DOM元素的style属性。style属性是一个对象,包含了可以读取和修改的CSS属性。比如,你可以像下面这样修改元素里的文本颜色。

// 不好的写法 element.style.color = 'red'; 

我们经常看到使用这种方法修改多个样式属性的代码端,比如:

// 不好的写法 element.style.color = 'red'; element.style.left = '10px'; element.style.top = '100px'; element.style.visibility = 'visible'; 

这种方法是有问题的,因为样式信息是通过JavaScript而非CSS来承载的。当出现样式问题的时候,通常是先去排除CSS问题,知道最后才会来排除JavaScript的问题。

开发者修改style对象还有一种方式,给cssText属性赋值整个CSS字符串,看下 面这个例子。

// 不好的写法 element.style.cssText = "color:red; left:10px; top:100px; visibility:visible"; 

使用cssText属性只是一次性设置多个值的一中快捷写法,这种模式还是有上一个问题存在

推荐

这里推荐是使用修改元素的className属性

.reveal{     color: red;     left: 10px;     top: 100px;     visibility: visible; } 

然后使用JavaScript添加至元素上

// 原生方法 element.className += " reveal";  // HTML5 element.classList.add("reveal"); 

有一种需要使用JavaScript计算位置,然后重新定位的功能,这个时候CSS不具备计算的能力,这个时候可以在CSS中写好了大部分代码,并设置好这个计算属性的默认值,然后是用JavaScript改变这个属性即可

将JavaScript从HTML中抽离

不要再HTML元素上绑定事件,这样在代码出现问题的时候,我们就可以去JavaScript文件中找对应的问题。这一点看起来无关紧要。但是“可预见性”会带来更快的调试和开发,并确定(并非猜测)从何入手调试bug,这会让问题解决得更快、代码总体质量更高。

将HTML从JavaScript中抽离

从服务器加载

第一种方法就是将模板放在远程服务器,使用XMLHttpRequest对象来获取外部标签。相对比多页应用,这种方法对单页应用带来了更多的便携。这种方法(从服务器中获取模板)很容易造成XSS漏洞,需要服务器对模板文件做适当的转义处理,比如<>以及双引号等,当然前端也应当给出与之匹配的渲染规则,总之这种方法需要一揽子前后端的转码和解码策略来尽可能的封堵XSS漏洞

当你需要注入大段的HTML标签到页面中时,使用远程调用的方式加载标签是非常有帮助的。出于性能的原因,将大量没用的标签存放在内存或者DOM中是很糟糕的做法。对于少量的标签段,你可以考虑采用客户端模板。

简单客户端模板

客户端模板是一些带“插槽”的标签片段,这些“插槽"会被JavaScript程序替换为数据以保证模板的完整可用。比如,一段用来添加数据项的模板看起来像下面这样。

<li><a href="%s">%s</a></li> 

这段模板中包含的%s占位符,这个位置的文本会被程序替换掉(这个格式和C语言中的sprintf()一模一样)。JavaScript程序会将这些占位符替换为真实数据,然后将结果注入DOM。

function sprintf(text) {     var i = 1, args = arguments;     return text.replace(/%/g, function() {         return (i < args.length) ? args[i++] : "";     }) }  // 用法 var result = sprintf(templateText, "/item/4", "Fourth item"); 

将模板文本传入JavaScript是这个过程中的重要一环。本质上讲,你一点也不希望在JavaScript中嵌入模板文本,而是将这个模板放置于他处。通常我们将模板定义在其他标签之间,直接存放在HTML中,这样可以被JavaScript读取,用两种方法都可以做到

  • 在HTML注释中包含模板文本(注释是和其他元素及文本一样的DOM节点,因此可以通过JavaScript将其提取出来)

    <ul id="myList"> 	<!--<li id="item%s"><a href="%s">%s</a></li>-->     <li><a href="/item/1">Frist item</a></li>       <li><a href="/item/2">Second item</a></li>     <li><a href="/item/3">Third item</a></li> </ul>     

    这段文档中,注释作为列表的第一个子节点,被恰当的放置于上下文中,下面的这段JavaScript代码则可以将模板文本从注释中提取出来

    var myList = document.getElementById("myList"),  templateText = myList.firstChild.nodeValue; 

    紧接着就将其格式化后插入DOM中。通过下面这个函数可以完成这些操作

    function addItem() {  var myList = document.getELementById("myList"),      templateText = myList.fristChild.nodeValue,      result = sprintf(template, url, text); 	  div.innerHtml = result;  myList.insertAdjacentHTML("beforeend", result); }  // 用法 addItem("/item/4", "Fourth item"); 

    我们给这个方法传入一些数据信息,用他们来处理模板文本,并用insertAdjacentHTML()将结果注入HTML。这一步操作将HTML字符串转换为一个DOM节点,并将它作为子节点插入到<ul>

  • 使用自定义type属性的<sript>元素。浏览器会默认地将<sript>元素中的内容识别为JavaScript代码,但是你可以通过给type赋值为浏览器不识别的类型,来告诉浏览器这不是一段JavaScript脚本

    <script type="text/x-my-template" id = "list-item">     <li> <a href="%s">%s</a> </li> </script> 

    你可以使用<sript>便签的text属性来提取模板文本。

    var script = document.getElementById("list-item"),     templateText = script.text; 

    这样addItem()函数就会变成这样

    function addItem() {     var myList = document.getELementById("myList"),         script = document.getElementById("list-item"),         templateText = script.text,         result = sprintf(template, url, text),         div = document.createElement("div"); 	     div.innerHtml = result.replace(/^s*/, "");     myList.appendChild(div.firstChild); }  // 用法 addItem("/item/4", "Fourth item"); 

    这里去掉了模板文中的前导空格。之所以会出现这个多余的前导空格,是因为模板文本总是在<script>起始标签的下一行。如果将模板原样注入,则会在<div>中创建一个文本节点,这个文本节点的内容就是个空格,而最后加入ul里的不是li,而是空格

复杂客户端模板

上几节中介绍的模板格式非常的简单,并无太多转义。如果你想用一些更健壮的模板,则可以考虑诸如【Handlebars】

所提出的解决方案。Handlebars是专为浏览器端JavaScript设计的完整的客户端模板系统。

Handlebars中的占位符为{{}},我们将上面的模板转化为Handlebars版本:

<li><a href="{{url}}">{{ text }}</a></li> 

在Handlebars模板中,占位符都标记为一个名称,以便可以在JavaScript中设置其映射。Handlebars建议将模板嵌入HTML页面中,并使用type属性为“text/x-handlebars-template”的script标签来表示。

<script type="text/x-my-template" id = "list-item">     <li> <a href="{{url}}">{{text}}</a> </li> </script> 

要想使用这个模板,你首先必须将Handlebars类库引入到页面,这个类库会创建一个名为Handlebars的全局变量,用来将模板文本编译为一个函数。

var script = document.getElementById("list-item"),     templateText = script.text,     template = Handlebars.compile(script.text); 

这时候,template包含一个函数,当执行这个函数的时候,返回一个格式化好的字符串。你只需要将name和url属性的对象传入

var result = template({     name:"Fourth item",     url:"/item/4" }) 

参数会自动做HTML转义,转义操作也是格式化的一部分。转义是为了增强模板的安全性,并确保简单的文本值不会破环你的标签结构。比如,字符串&会自动转义为&amp;

现在将他们合并为一个单独的函数中。

function addItem() {     var myList = document.getELementById("myList"),         script = document.getElementById("list-item"),         templateText = script.text,         template = Handlebars.compile(script.text),          div = document.createElement("div"),         result; 	     result = template({         name:"Fourth item",         url:"/item/4"     })              div.innerHtml = result;     myList.appendChild(div.firstChild); }  // 用法 addItem("/item/4", "Fourth item"); 

这个简单的例子并未真正体现Handlebars的灵活性。除了简单的占位符替换之外,Handlebars模板同样支持一些简单的逻辑和循环。

{{#if items}} <ul>     {{#each items}}      <li><a href="{{url}}">{{text}}</a></li>     {{/each}} </ul> {{/if}} 

if,作为判断,如果items里有数据才开始渲染,each循环

// 返回一个空字符串 var result = template({     items: [] });  // 返回包含两个记录的HTML列表 var result = template({     item:[         {         	text:"First item",             url:"/item/1"     	},         {         	text:"Second item",             url:"/item/2"     	}     ] })