任意一段重要的代码都需要关注无数的开发问题。但是,其中对可复用 JavaScript 代码挑战最大的五项问题如图 14.2 所示。
图 14.2 对可复用 JavaScript 代码挑战最大的五项问题
五大开发问题如下。
- 浏览器缺陷。
- 浏览器的缺陷修复。
- 外部代码。
- 浏览器回归。
- 浏览器缺失的功能。
我们需要权衡解决这些问题所花费的时间与得到的收益。这些是不得不回答的问题。你分析潜在受众、开发资源、开发排期等,这些都是决定性因素。
当试图开发可复用的 JavaScript 代码,我们需要考虑所有的因素,还需要考虑目前最流行的浏览器,因为这些浏览器是我们的目标受众最可能使用的浏览器。其他不那么流行的浏览器,我们至少保证代码可以优雅降级。例如,如果一个浏览器不支持某 API,我们应该小心我们的代码不会抛出任何异常,这样剩下的代码仍然可以顺利执行。
在接下来的小节中,我们将讲解这些问题,以便更好地理解我们面对的挑战以及如何应对。
1 浏览器的 bug 和差异
当我们开发可复用性 JavaScript 代码时,需要考虑解决的问题之一是处理我们确定需要兼容的多种浏览器 bug 以及 API 的差异。尽管浏览器越来越标准化,但是代码还是必须得完全符合浏览器提供的特性。
实现这一目标的方法很直接:我们需要完整的测试工具,足以覆盖代码常用的和不常用的用例。充分测试之后,在知道开发的代码将在支持的浏览器中工作后,我们会感到安全。假设浏览器没有后续变化,不会打破向后兼容性,我有一个模糊的预感,代码甚至会在未来版本的浏览器中工作。在 14.3 节中,我们会观察特定的策略来处理浏览器 bug 和差异。
复杂的地方是,当前浏览器 bug 会在未来的浏览器版本中被修复。
2 浏览器的 bug 修复
浏览器永远存在特定的错误是很愚蠢的——大部分浏览器 bug 最终都会修复,把希望寄托在浏览器 bug 上是很危险的开发策略。最佳方式是使用 14.3 节中的技术,使用不会过时的变通方案。
在编写一个可重用的 JavaScript 代码时,我们希望它可以持续运行很长时间。编写任何方面的网站(CSS、HTML 等),浏览器发布新版本后,我们不希望再回去修复代码。
假设浏览器 bug 引起常见的网站问题:为解决浏览器 bug 使用特殊技巧,将来浏览器发布新版本修复了 bug,就会出现问题。
处理浏览器漏洞的问题是双重的:
- 当 bug 最终被修复,我们的代码容易损坏。
- 我们无法为了避免网站损坏而说服浏览器厂商不修复 bug。
最近恰好发生了第 2 种情况的有趣的事例,关于 scrollTop 的 bug(https://dev.opera.com/article…)。
当处理 HTML DOM 时,可以使用 scrollTop 和 scrollLeft 属性,修改当前元素的滚动位置。但是当我们对根元素使用这些属性时,根据规范,将会返回滚动的位置,IE11 与 Firefox 浏览器严格遵循了这则规范。而 Safari、Chrome 和 Opera 并没有遵守。如果试图修改根元素的滚动位置时,不会发生任何事情。为了实现相同的效果,我们只能在 body 元素上使用 scrollTop 和 scrollLeft 属性。
当面对浏览器的不一致性时,Web 开发者们常常检测当前浏览器的名字(通过用户代理字符串,后续会详细介绍),然后在 IE11 和 Firefox 上对 HTML 元素使用 scrollTop 和 scrollLeft 属性,而在 Safari、Chrome 和 Opera 上则对 body 元素使用 scrollTop 和 scrollLeft 属性。规避这类问题将会造成灾难性后果。因为许多网页明确编码指定在 Safari、Chrome 或 Opera 上使用 body 元素,这些浏览器无法真正修复这个 bug,因为一旦修复,许多网页都无法运行。
这引出了另一个关于 bug 的观念:在确定某一功能是否是潜在的错误时,使用规范进行验证!
浏览器的 bug 不同于未指明的 API。参考浏览器规范非常重要,因为规范提供了确切的标准,浏览器使用这些标准进行开发和完善代码。相比之下,一个未指明的 API 的实现可能会在任何时候发生改变(特别是试图成为标准化的实现)。在未指明的 API 不一致的情况下,你应该对预期输出进行测试。警惕这些 API 未来可能发生的变化。
另外,bug 修复和 API 的变化是有区别的。bug 修复是很容易预见的——浏览器最终将修复 bug,即使要花很长的时间,API 变化更难发现。标准 API 不太可能改变,尽管不是完全闻所未闻,变化更有可能出现未指明的 API 中。
幸运的是,大多数 Web 应用程序出问题的情况很少发生。万一出现问题,有效地提前预知是无效的办法(除非我们逐一测试相关的 API——但是这样一个过程的开销是可怕的)。这种 API 的变化应该做回归处理。
下一个需要关心的问题是,没有人是一座孤岛,我们的代码也不是,让我们研究代码的影响范围。
3 外部代码和标记
任何可重用代码必须与围绕它的代码共存。我们希望代码运行在自己编写的网站或是他人开发的网站上,我们都需要确保代码可以与其他代码共存。
这是一把双刃剑:我们的代码不仅必须能够经受住可能写得很遭的外部代码,还必须得克服环境对代码的不利影响。
我们需要警惕的程度很大程度上取决于所使用的代码对环境的关注。例如,如果我们仅为单个或有限个网站编写可重用的代码,在某种程度上可以控制,可以少一些担心,因为我们知道代码的运行对外部代码的影响程序,而且一旦有问题,我们可以自行修复。
{注意}
这个问题的重要程度足以用一本书来阐述。如果你想更深入地探究,我们强烈推荐 Ben Vinegar 和 Anton Kovalyov 编写的《第三方 JavaScript》一书(Manning, 2013, https:// www.manning.com/books/third-party-javascript)。
如果开发代码将广泛用于未知环境(不可控的)中,则我们需要双重确认代码的健壮性。接下来讨论一些实现代码健壮性的策略。
代码封装
为了避免我们的代码影响页面上的其他代码,最佳实践是使用封装。通常来说,封装指代码(如同)存放在容器里。从广义上来说,是一种限制访问其他对象组件的语言机制。Aunt Mathilda 也许会总结为“各人自扫门前雪,莫管他人瓦上霜”。
在页面上引入我们的代码时,尽可能少地影响全局代码,将会使 Aunt Mathilda 非常开心。事实上,尽可能少地使用全局变量,甚至最好仅限一个,是很容易的。
第 12 章中的 jQuery,它是最流行的客户端 JavaScript 库,也是最好的范例。jQuery 引入一个名为 jQuery 的全局变量(一个函数),别名为 $,它甚至允许其他网页为 $ 设置别名避免冲突。
jQuery 中几乎所有的操作都通过 jQuery 函数完成。其他函数(工具函数)被定义为 jQuery 的属性(第 3 章介绍如何将函数定义为另一个函数的属性),使用 jQuery 作为命名空间。我们可以使用相同的策略。假设我们需要定义一组函数,我们将其定义在命名空间 ninja 下。
与 jQuery 类似,我们可以定义名为 ninja() 的全局函数以操作传入的变量。例如:
var ninja = function(){ /* implementation code goes here */}
使用我们设定好的命名空间定义工具函数:
ninja.hitsuke = function(){ /* code to distract guards with fire here */}
如果我们不需要 ninja 作为函数,仅作为一个命名空间即可,我们可以使用如下定义方式:
var ninja = {};
创建空对象,随后在该对象上定义属性或方法即可。为了保证代码的封装,需要避免其他操作,如修改已经存在的变量、函数原型甚至 DOM 元素。修改我们自己代码之外的任何内容,都可能引起潜在的冲突和混淆。另外,尽管我们小心翼翼地严格遵守最佳实践封装代码,但我们仍然无法保证代码的行为。
模范代码
有一个老笑话 Grace Hopper 在 Cretaceous 时期为接替人员清除蛀虫时说:“你最不恶心的代码就是你自己写的代码。”看起来很讽刺,但是当我们的代码与不可控的代码同时运行时,为了安全起见,我们需要假设最糟的情况。
尽管一些代码编写工整,但也有可能潜在地做一些出乎意料的事,例如修改函数属性、对象属性和 DOM 元素的方法。这些都可能设有陷阱。
在这种情况下,我们的代码只能做一些无伤大雅的事,例如使用 JavaScript 数组,一般情况下 JavaScript 数组只能是 JavaScript 数组。但是,如果一些页面上修改了数组的行为,我们的代码将无法运行,当然不是我们自身的原因。
遗憾的是,处理这种问题没有固定的原则标准,但是我们可以采取一些措施。我们将在后续小节中介绍保护性方法。
应对 ID 滥用
大部分浏览器具有一些反特性(我们不能称之为 bug,因为这些特性是有意而为之),这些特性会使得代码不可预期地落入陷阱从而运行失败。这些特性使得原始元素与添加在元素上的 id 或 name 属性产生关联。但是当 id 或 name 属性与元素上已经存在的部分属性产生冲突时,就会发生一些意料之外的情况。
查看以下 HTML 代码片段,观察 id 属性的滥用:
<form id="form" action="/conceal">
<input type="text" id="action"/>
<input type="submit" id="submit"/>
</form>
现在,在浏览器中可以这样调用:
var what = document.getElementById('form').action;
我们期望返回合理的 form 的 action 属性。大部分情况下是可以返回的。但是当检查值的时候你会发现,返回的却是 input#action 元素。为什么?让我们试试其他元素:
document.getElementById('form').submit();
这条语句本应引起 form 提交,但是却返回 script 错误:
Uncaught TypeError: Property 'submit' of object #<HTMLFormElement> is not a function
发生了什么呢?
浏览器将 <form> 元素内所有 input 元素都作为表单 form 的属性。这一特性开始看起来很便利,添加到 form 属性的名称是 input 元素的 id 或 name 属性。如果 input 元素的 id 或 name 属性恰好使用了 form 元素的属性,例如 action 或 submit,这些 form 元素的初始属性就被替换为新的属性值,通常被错误地指向 DOM。因此,在 input#submit 元素创建之前,form.action 的引用应指向 <form> 的 action 属性。在 input#action 元素创建之后,form.action 的引用指向 input#action 元素。form.submit 属性也发生了相同的情况。
这是为了兼容过去的浏览器的处理方法,老式浏览器不具备获取 DOM 元素的方法。浏览器厂商添加这种特性是为了方便获取 form 元素。现如今我们可以轻松地获取 DOM 元素,但是仍然留下了副作用。无论如何,浏览器这种特殊的“特性”可能引起代码中大量扑朔迷离的问题,在调试时需要谨记于心。当我们遇到属性被意外地转变成非预期的内容时,罪魁祸首有可能是 DOM 滥用。幸好我们可以在自己的代码中避免这种问题,避免编写有可能与标准属性发生冲突的过于简单的 id 或 name 属性,并可以推荐其他开发者使用类似的策略。开发过程中尤其需要避免 submit 值,以免造成令人沮丧和困惑的 bug 行为。
样式和脚本的加载顺序
通常我们期望 CSS 规则在代码执行时已经可用。确保在样式代码中定义的 CSS 规则在 JavaScript 代码执行时已经可用的最佳方式之一是,将外部样式表单放置在外部脚本文件之前。如果不这样做,可能引起意料之外的结果,因为脚本可能试图访问未定义的样式信息。遗憾的是,这种问题无法通过 JavaScript 脚本进行矫正,只能通过手动修改用户文件解决。
后续几节中会介绍一些关于外部代码对于代码运行的影响的基础示例。当其他用户试图将我们的代码集成进他们的网站时,会暴露出一些问题,那么应该如何诊断这些问题,如何设计合适的测试用例来解决这些问题呢?有时,当我们试图将其他人的代码集成进自己的页面时,会发现类似的问题,希望本节介绍的建议有助于解决这些问题。糟糕的是,对于解决代码集成问题,除了采用一些聪明的方式来编写防御性代码,没有其他更好的方式。接下来继续关注下一个问题。
4 回归
回归是在编写可复用、可维护性 JavaScript 代码时,遇到的最难的问题之一。因为浏览器的 bug 或不向后兼容的 API 发生变化(通常是未详细说明的 API)导致代码不可预期地中断了。
{注意}
这里我们使用术语回归的经典定义:过去使用的特性不再运行了。这通常是无意的,也有可能是仔细考虑后的结果。
预期的变化
一些 API 发生的可预见性的变化,我们可以提前检测并处理,如代码清单 14.1 所示。例如,Microsoft 在 IE 9 引入对 DOM 2 的事件处理机制(使用 addEventListener 方法绑定事件),而过去的 IE 版本使用 IE 内置的 attachEvent 方法。对于 IE 9 之前的代码,使用简单的特性检测可以处理这种变化。
清单 14.1 预期即将发生的 API 变化
function bindEvent(element, type, handle) {if (element.addEventListener) {element.addEventListener(type, handle, false); ⇽--- 使用标准 API 绑定
}
else if (element.attachEvent) {element.attachEvent("on" + type, handle); ⇽--- 使用专有 API
}
}
在本例中,不会过时的代码提前预知 Microsoft 将在 IE 浏览器中引入 DOM 标准。使用特性检测来判断浏览器是否支持标准 API,若支持,则使用 addEventListener 方法。如果不支持,则检测是否支持 attachEvent 方法。
大部分未来的 API 变化是不容易预测到的,并且无法预测未来的 bug。这是本书强调测试的最重要原因之一。面对不可预期的变化对我们代码的影响,最佳实践是在浏览器发行的版本中模拟测试,以快速发现问题。
使用优秀的测试套件并密切关注即将发行的浏览器版本是处理未来的退化问题的最佳方式。不是在开发周期中,而是在日常测试中进行。在新发行的浏览器版本中运行的这些测试,应该分解到开发周期中进行。
从以下网站可以获取即将发行的浏览器信息。
- Microsoft Edge (继承 IE): http://blogs.windows.com/msed…。
- Firefox: http://ftp.mozilla.org/pub/fi…。
- WebKit (Safari): https://webkit.org/nightly/。
- Opera: https://dev.opera.com/。
- Chrome: http://chrome.blogspot.hr/。
勤奋很重要。因为我们无法完全预测浏览器未来可能产生的 bug,可行的最佳方式就是对未来可能发生的情况时刻保持警惕。
浏览器厂商为了避免回归问题的发生,做了很多事情。浏览器通常将 JavaScript 库的测试套件集成进浏览器测试套件中,确保未来的回归不会直接影响这些库。虽然无法覆盖所有的问题(肯定无法完全覆盖),但这是一个很好的开端,表明浏览器厂商在尽可能地避免发生那样的情况。
在本节中,我们介绍了开发可复用性代码时面对的 4 种主要问题:浏览器 bug、浏览器 bug 修复、外部代码、浏览器回归。
本文摘自:JavaScript 忍者秘籍(第 2 版)
[美] John Resig(莱西格),Bear Bibeault(贝奥特),Josip Maras(马瑞斯)著
- jQuery 之父 John Resig 经典力作全新改版
- JavaScript 高手进阶秘籍
- 全面修订以涵盖 ES6 和 ES7 的概念
《JavaScript 忍者秘籍(第 2 版)》使用实际的案例清晰地诠释每一个核心概念和技术。本书向读者介绍了如何掌握 JavaScript 核心的概念,诸如函数、闭包、对象、原型和 promise,同时还介绍了 JavaScript API,包括 DOM、事件和计时器。你将学会测试、跨浏览器开发,所有这些都是高级 JavaScript 开发者应该掌握的技能。
本书包含以下内容:
- 使用函数、对象和闭包编写更高效的代码;
- 学会避免 JavaScript 应用陷阱;
- 使用正则表达式编写简洁的文本处理代码;
- 使用 promise 管理异步代码;
- 全面修订以涵盖 ES6 和 ES7 的概念。
在本节中,我们介绍了开发可复用性代码时面对的 4 种主要问题:浏览器 bug、浏览器 bug 修复、外部代码、浏览器回归。