编程实践 - 第 5 章 UI层的松耦合
《编写可维护的 JavaScript》—— Nicholas C. Zakas
在 Web 开发中,用户界面(User Interface,UI)是由三个彼此隔离又相互作用的层定义的。
- HTML 定义页面的数据
- CSS 给页面添加样式,创建视觉特征
- JavaScript 给页面添加行为,使之更具交互性
三者之间的关系(Web UI 分层)
|------------------------|
| CSS | JavaScript |
|------------------------|
| HTML |
|------------------------|
CSS、JavaScript 都依赖 HTML。
1. 什么是松耦合
很多设计模式就是为了解决耦合的问题。
如果两个组件耦合太紧,则说明一个组件和另一个组件直接关联,
这样的话,如果修改一个组件的逻辑,那么另外一个组件的逻辑也需要修改。
比如,有一个 .error
的 CSS 类,在很多页面都用到了,你突然觉得 .error
不合适,想改名为 .warning
。
这时,你不仅需要改动 CSS 文件,还需要改动所有用到该 CSS 类的 HTML 文件。
这说明 HTML 和 CSS 紧耦合。
当你能够做到修改一个组件而不需要改动其他组件时,你就做到了松耦合。
对于多人大型系统来说,有很多人参与维护代码,松耦合对于代码可维护性来说至关重要。
你绝对希望开发人员在修改某部分代码时不会破坏其他人的代码。
当一个大系统的每个组件的内容有了限制,就做到了松耦合。
本质上讲,每个组件需要保持职责单一来确保松耦合。
组件知道的越少,就越有利于形成整个系统。
需要注意:在一起工作的组件无法达到“无耦合”(no coupling)。
在所有系统中,组件之间总要共享一些信息来完成各自的工作。
我们的目标是确保一个组件的修改不会经常性地影响其他部分。
如果一个 Web UI 是松耦合的,则很容易调试。
- 文本或结构相关的问题,查找 HTML 即可
- 样式相关的问题,查找 CSS 即可
- 行为相关的问题,查找 JavaScript 即可
2. 将 JavaScript 从 CSS 中抽离
在 IE8 以及之前的版本中,可以使用 CSS 表达式(CSS expression)。
CSS 表达式允许你将 JavaScript 直接插入 CSS 中,如下
/* 不好的写法 */
.box {
width: expression(document.body.offsetWidth + 'px');
}
CSS 表达式被包裹在一个特殊的 expression()
函数中,可以给它传入任意 JavaScript 代码。
浏览器会以高频率重复计算 CSS 表达式,这严重影响了性能。
幸运的是 IE9 不再支持 CSS 表达式了。
3. 将 CSS 从 JavaScript 中抽离
保持 CSS 和 JavaScript 之间清晰的分离。
将 CSS 类选择器作为 CSS 和 JavaScript 之间通信的桥梁。
JavaScript 通过 element.className
随意添加和删除 CSS 类选择器;
CSS 类选择器的样式在任何时候都是可以修改的,且不必更新 JavaScript。
JavaScript 不应当直接操作样式,以便保持和 CSS 的松耦合;
如果要给某个元素重新进行绝对定位,则还是需要使用到 style.top
直接操作样式。
// 不好的写法
element.style.color = 'red';
element.style.background = 'yellow';
// 不好的写法
element.style.cssText = 'color: red; background: yellow;';
// 好的写法
/*
.active {
color: red;
background: yellow;
}
*/
element.className =+ ' active';
element.classList.add('active');
4. 将 JavaScript 从 HTML 中抽离
<!-- 不好的写法 -->
<button onclick="doSomething()">Click Me</button>
通过 on 属性来绑定事件处理程序,造成两个 UI 层(HTML 和 JavaScript)的深耦合。
存在两个问题:
- 当点击按钮时,
doSomething()
函数必须存在 - 修改
doSomething()
的函数名时需要同时修改两处(典型的紧耦合)
<!-- 不好的写法 -->
<script>
function doSomething(){
// ......
}
</script>
绝大多数 JavaScript 代码都应该包含在外部文件,通过 <script src="1.js">
引入。
确保在 HTML 代码中不会有内联的 JavaScript 代码。
这么做的原因是出于紧急调试的考虑的。
当 JavaScript 报错,你的下意识行为应当是去 JavaScript 文件中查找原因。
如果在 HTML 中包含 JavaScript 代码,则会打断你的调试流程,而且 HTML 中的代码也不好打断点调试。
可预见性(Predictability)会提高调试和开发效率,并确信从何入手调试 bug,这会让问题解决得更快、代码总体质量更高。
5. 将 HTML 从 JavaScript 中抽离
最好将 HTML 从 JavaScript 中抽离。
当需要调试一个文本或结构性问题时,你更希望从 HTML 开始调试。
// 不好的写法
var div = document.getElementById('myDiv');
div.innerHTML = '<h3>Error</h3><p>Invalid email address.</p>';
将 HTML 嵌入在 JavaScript 代码中是非常不好的实践,原因如下
- 增加了跟踪文本和结构性问题的复杂度。调试时需要对比 DOM 树和 HTML 源码。
- 一旦 JavaScript 做了复杂的 DOM 操作,就很难追踪 bug。
将 JavaScript 从 HTML 中抽离的方法是使用模板引擎
- 后端:PHP 文件、JSP 文件
- 前端:Mustache、Handlebars
一旦你需要修改文本或标签,只需要去模板中修改就行,而不必在 JavaScript 中修改。
5.1. 方法 1 - 从服务器加载
将模板放置于远程服务器,使用 XMLHttpRequest
对象来获取大段的外部标签。
这种方法容易造成 XSS 漏洞,需要服务器对模板文件做转义处理,如 <
、>
、"
等。
相比于多页应用(multiple-page applications),这种方法对于单页应用(single-page applications)带来更多的便捷。
比如,点击一个链接,希望弹出一个新对话框,代码如下:
function loadDialog(name, oncomplete) {
var xhr = new XMLHttpRequest();
xhr.open('get', '/js/dialog' + name, true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4 && xhr.status === 200) {
var div = document.getElementById('dlg-holder');
div.innerHTML = xhr.responseText;
oncomplete();
}
};
xhr.send(null);
}
// jQuery
function loadDialog(name, oncomplete) {
$('#dlg-holder').load('/js/dialog' + name, oncomplete);
}
这里没有将 HTML 字符串嵌入在 JavaScript 里,
而是向服务器发起请求获取字符串,这样可以让 HTML 代码以更合适的方式注入到页面中。
当你需要注入大段 HTML 标签到页面中时,使用远程调用的方式来加载标签是非常有帮助的。
出于性能的原因,将大量没用的标签存放在内存或 DOM 中是很糟糕的做法。
对于少量的标签段,你可以考虑采用客户端模板。
5.2. 方法 2 - 简单客户端模板
客户端模板是一些带“占位符”的标签片段,这些“占位符”会被程序替换为数据以保证模板的完整可用。
比如,一段用来添加数据项的模板看起来就像下面这样
<li><a href="%s">%s</a></li>
这段模板中的 %s
占位符会被程序替换掉。
JavaScript 程序会将这些占位符替换为真实数据,然后将结果注入 DOM。
如下:
function sprintf(text) {
var i = 1, args = arguments;
return text.replace(/%s/g, function() {
return (i < args.length) ? args[i++] : '';
});
}
var result = sprintf(templateText, '/item/4', 'Fourth item');
而如何获取模板文件(templateText
)呢?
模板不会嵌入 JavaScript,而是置于他处。
通常将模板存放在 HTML 页面,这样可以被 JavaScript 读取。有两种方法:
- 放在注释里
- 放在
<script type="text/x-my-template" id="list-item">
里。
读取注释里的模板
<ul id="mylist"><!--<li id="item%s">%s</li>-->
<li id="item1">Frist</li>
<li id="item2">Second</li>
</ul>
var mylist = document.getElementById('mylist');
var templateText = mylist.firstChild.nodeValue;
读取 <script id="list-item">
中的模板
<script type="text/x-my-template" id="list-item">
<li id="item%s">%s</li>
</script>
var script = document.getElementById("list-item");
var templateText = script.text;
5.3. 方法 3 - 复杂客户端模板
推荐使用 Handlebars
- 占位符使用
{{ text }}
- 会对传入的文本值做转义,以确保安全性,以及不破坏模板的结构
- 提供流程控制语句,是一款强大的 JavaScript 模板引擎