《编写可维护的 JavaScript》编程实践 - 第 5 章 UI层的松耦合

编程实践 - 第 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 读取。有两种方法:

  1. 放在注释里
  2. 放在 <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 模板引擎
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值