43. 非干扰性 JavaScript 的原则
引言
在前面的文章中我们讨论了 HTML 和 CSS 以及如何正确地使用它们,我们还探讨了 JavaScript 的基础知识—— JavaScript 是什么,能够用来做什么,以及为什么你应该了解它。在我们开始详细地探讨如何实际应用 JavaScript 之前,我们需要停下来思考一下如何聪明地使用 JavaScript ,从而确保不会因为它的缘故使任何人无法访问你的网站。这就是非干扰性的 JavaScript 背后的核心理念。为了更好地理解什么是非干扰性 JavaScript 以及我们为什么需要它,我们先来看看在 JavaScript 程序设计中会碰到哪些不确定性因素:
- 因为某些浏览器不支持 JavaScript 或它们的支持的 JavaScript 版本太老了,这些浏览器可能会完全忽略你的脚本。
- 即使一个浏览器可以支持 JavaScript ,用户们也可能会出于安全性的考虑而禁用 JavaScript ,用户所在公司的防火墙也可能会通过移除所有的
<script>
标签来阻止 JavaScript 的运行。 - 即便一个浏览器支持 JavaScript ,它也可能不理解 DOM 规范中的某些浏览器专有特性( IE 经常被认为是这方面的祸首),这会导致该浏览器无法理解你的部分脚本。
- 即便是脚本能得到正确地解释,它也可能要依赖于非常复杂的 HTML ,并且/或者依赖于可能以无法预测的方式被改变的 HTML 。
- 就算 JavaScript 脚本运行在完美无误的 HTML 中,你也不能确定你的用户将会用什么类型的输入设备。许多脚本只有在用户使用鼠标时才会生效,而对那些使用键盘的人群则没有反应(许多残障用户无法使用鼠标,而且有的人偏偏喜欢用键盘)。
- 即使你的脚本回避了上述的所有风险而且运行得很好,其他的程序员们也有可能读不懂它。
这真是一条很长的问题清单,大多数问题从本质上来说并不是技术性的,而是概念性的问题,或者,如果你愿意这么说的话,是哲学上的问题。此外,尽管最后两个问题也可能会发生在其它程序设计语言中,但前面四个则是 JavaScript 程序设计环境所独有的问题,因此编程经验在这里也帮不上忙。
总之,非干扰性的 JavaScript 是一种编写 JavaScript 的方式,通过这种方式你网站的访客们就不会由于上述原因而被挡在你的网站外面了——即使你的 JavaScript 不能恰当地为他们效力,这些访客也仍然能够使用你的网站,尽管他们只能使用更为初级的功能。本文将会讨论非干扰性的 JavaScript 的原理和实际应用,并会为本教程随后的学习打下坚实的基础。
本文结构如下:
非干扰性 JavaScript 的定义
为了有效地解决上面列出的问题,了解你在做什么以及这么做的原因是极其重要的。我们需要一套关于正确开发 JavaScript 的理论。
非干扰性的 JavaScript 提供的就是这样的理论。它本身并不是一种技术;非干扰性的 JavaScript 并不是在你的脚本中添加一个 makeUnobtrusive()
函数来使你一劳永逸。它其实是一种思维方式。为了确保你的脚本不会给任何人添麻烦,你应该确保你的脚本不需要任何前提假设。
就目前而言,“不要做任何假设”实在有点苛刻了。幸运的是我们可以将非干扰性归为三类:你的脚本不应该干扰用户、浏览器、或同事程序员。
- 假设:每个人的浏览器都支持 JavaScript 。错:为了达到不干扰用户的效果,应该做到去掉脚本也不会妨碍他们使用你的网站,即使这些用户能够使用的交互会比那些浏览器能够支持 JavaScript 的用户减少很多。
- 假设:所有浏览器的工作方式都是一样的。错:为了做到不干扰浏览器,你的脚本应该避开错误和兼容性问题,并要考虑到诸如语音浏览器或移动电话之类的特殊设备。
- 假设:每个人都能读懂我的代码。错:为了做到不干扰程序员搭档,你的脚本应该由简洁明晰的代码组成,并且添加了大量的注释,来解释某段代码是(打算)做什么用的。
本文将会详细讨论前两个假设。第三类问题将作为单独的一篇文章来进行探讨
除了规避这些假定之外,非干扰性的 JavaScript 还要求你将自己的 JavaScript 代码从 HTML 之中分离出来。由于这一点是这个程序设计哲学中技术性最强的部分,我们就先来讲讲这方面的内容,然后再去讨论那些假设。
分离结构与行为
就像我们将结构与外观分离开一样(通过将所有的 CSS 放在一个单独的文件中,并避免style
属性或其它类似的表示性标记的使用,),我们也应该将 HTML 结构和 JavaScript 行为分离开来。这么做的理由是一样的:它可以将你所关注的焦点独立出来,保持你的代码整洁,而且可以让你致力于 JavaScript 上的工作,而又不必牵扯到 HTML 或 CSS 。
基本规则很简单:在 HTML 文件中不要包含任何 JavaScript ,就像不要包含任何 CSS 一样。 JavaScript 开发者常放在其 HTML 文件中的两样东西是:嵌入式脚本和事件处理器。实现结构与行为分离的最简单的方法就是将这两样东西移走。
就如你在前一篇文章中所学到的那样,嵌入式脚本就是在一个 HTML 页面内的 <script>
标签之间的 JavaScript 代码片段。这些片段可以很方便地移到一个外部 JavaScript 文件中,所以移除嵌入式脚本是不成问题的。
内联事件处理器,比如 <a href="/somewhere. HTML " onmouseover="hideAll()">
,其移除难度稍高一些。这个语句所定义的是当一个特定事件发生的时候,应该运行哪个事件处理器( JavaScript 函数)。为了使这段代码符合非干扰性,我们需要将该事件处理器的指派放到一个单独的脚本文件中去。这就意味着外部脚本需要先找到正确的元素,然后将一个事件处理器指派给它。
欲了解关于事件处理器的更多信息,请参阅处理 JavaScript 事件。
使某个元素易于查找的最简单的方法就是给它赋一个ID。例如:
<!-- HTML : -->
<a href="/somewhere. HTML " id="somewhereLink">
<!-- JavaScript : -->
var x = document.getElementById('somewhereLink');
if (x) {
x.onmouseover = hideAll;
}
只要用户的鼠标经过该链接, hideAll()
函数就会被执行,并且这段代码是等价于 <a href="/somewhere. HTML " onmouseover="hideAll()">
的。
更妙的是,这两行代码在每个包含 id="somewhereLink"
的元素的页面中都有效,这样你就不必一再地重复这段代码了。而且即使页面中不包含这样的元素, if (x)
检查语句也能确保不会产生错误的信息:只有在x元素真正存在的情况下,该语句才会给它指派一个事件处理器。
那么,要是用户的浏览器不支持 JavaScript 又会怎样呢?显然,在这种情况下鼠标经过效果就不会起作用了。但链接还是链接,仍可以跳转到该链接。
现在,我们的代码除了变得更整洁之外,也变得更易于维护了。
举例来说,在大多数情况下配合使用 mouseover
事件与 focus
事件是个不错的办法。mouseover
只在用户使用鼠标的情况下才能起作用。不用鼠标的人会用键盘来聚焦在他们想要点击的链接上,这样就会触发 focus
事件。如果我们为该事件注册了事件处理函数(也就是说,如果我们告知该函数,它应被这个事件所触发)的话,我们就确保了自己的应用程序也可以对键盘做出响应。
如果妥善的分离了脚本与 HTML ,上述要求是很容易做到的:
var x = document.getElementById('somewhereLink');
if (x) {
x.onmouseover = x.onfocus = hideAll;
}
只需要添加12个字符,我们的应用程序的这一部分可以被键盘所访问了。很简单,对吧?
现在我们来思考一下,如果我们使用了内联事件处理器的话,又应该怎么做。我们需要手动地遍历整个网站上所有的链接,并且将 onfocus="hideAll()
添加到带有 onmouseover
事件处理器所有链接中。这样一来,我们不仅会浪费掉许多本可以用在更有意义的事情上的时间,而且这样做还是一种易于犯错误的工作方法,因为很容易就会漏掉位于某个不显眼的模板中的某个链接,而这种模板仅用于非常特殊的情况;或者是直接输错某条 onfocus
语句。
因此,正如将 HTML 和 CSS 分离开来是一个明智之举,将 HTML 和 JavaScript 分离开也同样是一个好办法。移走内嵌脚本和内联事件处理器是很简单的事,而且这样做会使你的代码质量和可维护性得到立竿见影的改善。
添加一个可用性层
既然我们已经解决了非干扰性 JavaScript 中技术性最强的部分,现在就该回到前面我们讨论过的假设上了。最重要的事情是,绝不要假设每个人的浏览器都支持 JavaScript ,而且这一点直接影响了在网站上添加脚本的目地。
JavaScript 的目的是在你的网站中添加一个可用性层。注意“添加”这个词:如果脚本就是整个可用性层(换句话说,如果这个网站离开了 JavaScript 就无法使用)的话,你就犯了一个严重的错误,而且你的脚本也不是非干扰性的了。
示例——表单校验
实际的例子可以更加清楚地说明这一点,因此我们来讨论一下表单校验。在添加到数据库中之前,应当总是在服务器上对表单中的用户输入进行检查,因为毫无保留地相信用户输入(可能是恶意的用户所输入的),这会导致你的网站更快,也更确定无疑地被黑掉——我们必须在接受数据之前对其进行检查。
然而,服务器端表单校验也有其缺点,那就是耗时要多一点。用户将表单提交给服务器,而服务器则生成一个响应页面,以通知用户提交成功或出错。无论服务器的反应多么快速,在用户提交和信息反馈之间都会有一小段延时——我们假设是半秒钟。
更糟的是,如果用户犯了错误——比如说没有填写她的地址——她就不得不更正这个问题,然后再次提交表单,这就会导致又一个半秒钟的等待。虽然实际上时间并不长,但却会在用户的感觉中形成累积效应,尤其是在她不得不来回折腾好几次的情况下。
这就是 JavaScript 大显身手的时候了。如果你添加了一个脚本,可以在表单发送到服务器之前对其进行检查的话,你就可以捕获典型的用户错误,同时还可以避免服务器和用户之间数据传输;你的用户界面的运行看起来也会显得更快,更平稳。
因此,使用 JavaScript 表单验证脚本是一个不错的主意。然而需要牢记的是——你只能将 JavaScript 作为服务器端表单验证的补充。前者能为你提供一个更流畅的界面,但只有后者才能为你提供适当的安全性(如果你打算调皮捣蛋的话,要绕开 JavaScript 是很容易的——你只需关掉 JavaScript 即可),后者还可以在用户浏览器不支持 JavaScript 的情况下发挥作用。
在2009年早期时候 (在不久的将来这可能会改变) Opera 浏览器相对其他浏览器有一个优势,就是支持 HTML 5 Web Forms 2.0 规范,这是更高级的处理 HTML 表单的方法,不需要 JavaScript 就可以实现客户端表单验证,因此验证过程无法被用户关闭。要更多了解 Web Forms 2.0,请参阅使用 HTML 5 改善 HTML 表单。
原则
这个例子强调了一些重要原则:
- 你的网站应当在没有 JavaScript 的情况下依然能工作。
- 如果 JavaScript 是启用的,你就可以为你的用户提供一个额外的可用性层;该可用性层可以让他们更快捷地完成自己的任务,而且可以最大限度地避免令人不快的页面重新载入。
- JavaScript 的安全性不佳。也就是说,对于检验表单中的用户输入之类的重要任务,你*绝不*应该信任纯 JavaScript 的程序。毕竟,一个蓄意的用户可以直接关掉 JavaScript 来绕过你的防御。
那么,精确地来说,“你的网站应该在没有 JavaScript 的情况下依然能够工作”的意思是什么呢?它的意思是任何用户,不管他用的是什么浏览设备和软件,都应该能读取你网站的内容,并能使用网站导航功能及其它关键性的功能。其意义就是这么简单,不比这个多,但也(更重要的是)不比这个少。
还有一个重要的结论是,不须为那些不适用脚本的用户提供与启用脚本的用户相同的功能。在表单验证的例子中,在得到自己的脚本提交结果之前,不使用脚本的用户只能等上半秒钟,因此不使用脚本的用户比启用了脚本的浏览器用户的用户体验要稍微差一点。
实际上,这是一个基本规则。如果 JavaScript 被禁用,可用性就会打折扣,而作为一个web开发者,你的工作就变成了确保人们可以使用你网站的基本功能:内容和导航。其它的网站功能就成了可选项。
示例——弹出式窗口
我们来看看另一个例子——弹出式窗口。有的时候弹出式窗口真的很有用,而且创建弹出式窗口也非常简单——如果浏览器支持 JavaScript 的话。然而,如果浏览器不支持 JavaScript ,那么使用该浏览器的用户就看不到弹出式窗口了。你该怎样在保证自己的网站可访问的同时,为启用了 JavaScript 的浏览器提供一个更流畅的界面呢?
用 HTML 来写: <a href="/somewhere. HTML " class="popup">Go somewhere</a> 用 JavaScript 来写: var x = document.getElementsByClassName('popup'); // 一个自定义函数 for (var i=0;i<x.length;i+=1) { x[i].onclick = function () { window.open(this.href,'popup','arguments'); return false; } }
此处最重要的部分就是链接的href
属性。这个属性定义了应该在弹出菜单中显示的页面,除此之外它还确保了在 JavaScript 被禁用的情况下,用户也能跟踪该链接。因此本示例在没有 JavaScript 的情况下仍然是完全可访问的——只不过没那么美观罢了。
如果 JavaScript 是启用了的,你就可以添加一个额外的可用性层:弹出式窗口。你可以找出所有带有 class="popup"
的链接,并添加一个 onclick
事件处理器,用来打开某个弹出式窗口。弹出式窗口该显示那个页面呢?就是在该链接的href
属性中定义的那个页面(this.href
)。
不同的例子,同样的原理。首先是确保任何用户都能访问该信息;然后在此之上添加一点 JavaScript 程序片段,来让该界面工作得更流畅。
示例——Ajax
这个理论有时会难以应用到实际中,尤其是如果你对 Web 开发不怎么熟而又想要创建,比方说,一个 Ajax 网站的话。弄懂非干扰性 JavaScript 有一个很有用的诀窍,那就是层的思想。这个网站的基本功能是什么?基本功能应当被放在最底层,该层有没有 JavaScript 都能访问。接下来,你可以在最底层之上应用任意数量的 JavaScript 驱动的可用性层,从而使该网站更易于使用。这些层不会干扰基本的网站功能,它们只是用来提供一些附加功能。
我们举个例子来阐明该基本规则。假设你的网站是关于移动电话比较的。那么该网站的基本功能差不多就是下面几样:
- 通过型号或诸如“支持无线上网”或“可与我的桌面电脑同步”这样的笼统的标准,来搜索某种移动电话。
- 得出一份与你的搜索条件相匹配的移动电话列表。
- 将该列表按价格,名称/型号,或相关性排序。
所有这些功能都可以不用任何 JavaScript 创建,并且你的首要任务就是实现这些功能。因此,你要创建:
- 一个搜索页面。
- 一个列表页面,由服务器生成,该页面是基于你的搜索结果的。
- 在该列表页面中添加链接,这些链接的作用是从服务器获取排序方法不同的列表。
当你做好了这些无脚本的页面,你就创建了一个几乎能够在任何设备上的任何浏览器中工作的基础层。
做好这些之后,你就可以在这些仅有基本功能的页面上添加 JavaScript 功能了。最显而易见的改善是排序页面。毕竟,排序所必需的数据——比如价格和特性——已经在该页面上的表格中了,因此你不必再往服务器跑一趟来获取这些数据了。你只需要写一个脚本来从该表格读取这些数据,然后将该表格的 <tr>
排序就可以了。
写完自己的脚本之后,你就应该确保用户能使用它。为达到这个目的,最显而易见的解决方案就是为该用户提供一个链接。
此处需要注意的一点就是表中已经有链接了:这些链接负责从服务器获取一份按照不同顺序来排序的列表。因此,目前为止最好的解决方案就是改写这些链接的默认行为,就像我们在弹出式菜单一例中所做的一样。
因此你可以编写一个附加功能来为这些链接指定 onclick
事件处理器,以确保在这些连接上点击鼠标时调用的是你的排序脚本,而不是服务器上的页面。
这样,所有的用户就都会看到相同的排序链接,但这些链接的真正行为取决于浏览器是否能处理附加的 JavaScript 的可用性层。如果能处理的话,该浏览器就会启动你的脚本,如果不能处理的话,则会从服务器获取一个新的页面。
改写 HTML 元素的默认行为是非干扰性的 JavaScript 的一个典型范例。如果用户的浏览器支持你的脚本,那当然好,你的脚本就会得到执行。如果不支持的话,就会回落到该 HTML 元素的默认行为。只要你遵循这个原则,你的脚本就符合非干扰性。
规则的,语义性的 HTML
脚本是在web页面的环境中运行的。那么是在哪种Web页面中运行的呢?从理论上讲任何页面都可以,只要页面中具备一些可以由你操作的 HTML 元素就行。不过实际上你很快就会发现,那些遵从标准的,使用了语义化而且结构化的 HTML 的,并且适当地分离了自身的 HTML 和 CSS 的 Web 页面要比那些所见即所得网页编辑器生成的,基于表格的页面容易处理得多。
假设你正在致力于一个新闻网站的开发,而你希望每个页面都包含一个可点击的内容表格,该表格中的内容是到新闻报道中的所有新闻标题的链接。要实现这个效果,最简单的办法是扫描出整个页面中的标题标签(<h1>
, <h2>
,,等等),创建一个列表,并确保在该列表上的点击可以将用户导向至正确的标题。脚本写好了,我们来看下一个工作。(顺便提一句,这个脚本也是非干扰性的。跳转至标题的功能是一种可有可无的附加功能,但如果没有该功能的话,用户也能通过导航来阅读该页面。)
现在假设不幸的是,你要开发的网站不是用你以前学过的语义性的 HTML 和 CSS 来创建的。假设它看起来像这样:
<div class="contentcontainer">
<p class="maincontent"><b class="headline">McCain suspends campaign to spend more time with his economy</b><br>
In a move that shocked political observers ... [etc etc]</p>
</div>
你还能为这样一个标记混乱的页面编写一个生成内容表格的脚本吗?当然能。只要依次遍历所有的 <b>
标签,并指明每个带有 class="headline"
的标签其实都是一个新闻标题——然后你的脚本就能起效了。
虽然上述方法也能生成内容表格,但实际上你还是丢失了一些信息,那就是新闻标题的结构化嵌套层级。一个规则的,语义性的页面能直接告诉人们 <h3>
标题是前面的 <h2>
标题的副标题,而你的脚本大可以对这个特性加以利用。
在一个像上面那样的非语义性的页面中,你需要找出其它办法来确认某个 <b class="headline"
是否是主标题,副标题,副副标题,或其是它什么东西。
现在,即使是上面说的那样的问题也有办法解决了,但这不是重点。重点是,如果该 HTML 的作者用了适当的标题标签来标记这些标题,你的脚本就一定可以找出与之相符合的信息,而不管这个脚本在何时运行,也不管是在何处运行的。相反,如果 HTML 的作者用的是 <b class="headline">
这样的标记,这就说明他并不真正知道自己在做什么,因为稍后该 HTML 的版本可能轻易地转变为 <strong> 或 <span> 之类的标签。每次 HTML 发生了改变,你都得修改自己的脚本。
因此,用来处理规则的,语义性的页面的脚本通常比较容易编写,而且通常比那些用来处理标签大杂烩的脚本的维护要容易得多。
浏览器兼容性
非干扰性的 JavaScript 也要求脚本不干扰浏览器。实际上,这意味着该脚本应当能够运行在尽可能多的浏览器上,而且如果不能运行的话,该脚本也不得导致脚本错误;该脚本应该通过柔性的功能衰减给网站用户留下一个仍然能提供有效功能的页面。这听起来很不错,但这即使对 JavaScript 大师们来说也是 JavaScript 开发的最难的问题之一。
JavaScript 高手 Douglas Crockford 把浏览器叫做“世界上最不友好的程序设计环境。”这不仅是因为浏览器并不是一个真正的应用程序平台(目前还不是),在提供调试器之类的工具方面有不良记录(尽管这方面也在渐渐改善),而且两者之间的巨大差异很快就会让 JavaScript 新手抓狂。
不幸的是,浏览器的不兼容性是无可争辩的事实。一旦你开始编写任何复杂的 JavaScript ,就会发现各种浏览器的行为之间差异非常大。最常见的情况是,你会发现你的脚本在除了 IE 的所有浏览器上都能用,尽管在其它的浏览器中也有各种小问题。
造成这些不兼容性的根本原因在于大多数的主流浏览器都有自己的 JavaScript 引擎,以解析、解释和执行你的脚本,而这些引擎是各不相同的。
不兼容的问题常常是来自于 IE ;有不少功能在 IE 中跟在别的浏览器上的运行结果是不一样的,或者 IE 干脆就完全不支持。微软 IE 团队致力于使自己的浏览器更加地符合标准,但这项工作还没有完成。所以你还是会遇到一些麻烦。
即便是这个问题解决了,也还是会有大量的不兼容问题存在,其原因可能仅仅是因为某一 JavaScript 引擎还不支持某种方法,或者是因为 JavaScript 引擎开发者无意中犯的一个错误。
因此兼容性问题是存在的,而且这个问题短时间内是不会消失的。那么我们怎么应对?
最佳策略就是接受它并集中精力对付关键的技术细节,本系列教程随后的文章会详细探讨这些细节。不幸的是要想记住所有的浏览器兼容性问题是不可能的,但对那些刚起步的 JavaScript 的程序员来说,还是有几点帮助建议的。在 QuirksMode.org上面有兼容性列表,该表格清楚地说明了哪种浏览器支持哪些方法和属性,并且还提供了大量的bug提示。
除此之外,许多人以前已经走过这条路了。其中一些人决定创建 JavaScript 程序库,以绕过这些兼容性问题。因此,利用 JavaScript 程序库来解决浏览器不兼容的问题是一个切实可行的办法。
虽然如此,还是要多加小心。作为一个 JavaScript 新手,你应该慎重考虑一下,如何在不利用程序库的情况下编写你的第一个大型项目。这么做的理由是一个成熟的 JavaScript 程序员必须要努力钻研,才能深刻了解在底层到底发生了什么。这样做你就能清楚地知道浏览器会怎样处理你的脚本,这些知识对 JavaScript 程序员的开发工作会很有帮助。
你当然可以用程序库来解决问题,但要抵制碰到问题立刻就使用程序库的诱惑。为了深入理解自己所面对的问题,你应该试着不用程序库来解决问题。
总结
非干扰性的JavaScipt不仅仅是一种技术,更是一种程序设计思想。其最核心是对于哪个功能属于哪一层有个清楚的认识。所有至关重要的网站功能都应当完全用 HTML 来编写,当你成功创建此基础后,就可以在其上添加一个 JavaScript 层,来为支持 JavaScript 的浏览器提供一个看起来更美观,更清爽,也更快捷的界面。
另外,非干扰性的 JavaScript
- 将结构和行为分离开了,这样就使得你的代码更简洁,脚本也更容易维护
- 绕开浏览器不兼容问题
- 处理的对象是规则的,语义性的 HTML 层r
非干扰性的 JavaScript 并不是那么难的。它主要就是一种思维方式;一旦你花几个小时来好好规划一个非干扰性的 JavaScript ,你就会发现做出正确的决策变得越来越容易了。
当你习惯了非干扰性的 JavaScript 之后,你就不会愿意再回到以前的那种干扰性的模式了。