Unobtrusive JavaScript 的七条规则
简述
在开发、教学和实现 Unobtrusive JavaScript 的工作中,我总结了下面七条规则。本文实际是关于 Unobtrusive JavaScript 的一次会议上(Paris Web conference 2007 ,巴黎)的发言稿提纲。
我希望本文能帮你理解为什么要如此编写 JavaScript 代码。它曾帮助我更快的交付产品、提高产品质量并减轻维护工作量。
1. 不要做任何假设 ( JavaScript, 不可靠的助手)
Unobtrusive JavaScript 最重要的规则就是停止做任何假设:
- 不要假设 JavaScript 一定可以使用,不应依赖 JavaScript 而应当把它当作助手。
- 不要假设浏览器支持某些方法及具有某些属性,在使用前测试是否可用
- 不要假设 HTML 代码是正确的,使用前最好先检查器正确性
- 保持功能独立于输入设备
- 你要考虑到其他脚本可能会影响你的代码,应让你的代码作用域尽可能安全
在开始编写脚边之前需做的第一件事就是查看要用脚本增强的HTML代码,并想想有哪些可以加以利用。
2. 找到钩子和关系(hook and relationship) (HTML 是脚本的基础)
在开始编写脚本之前先看看 HTML 文档。如果 HTML 结构混乱,很难为之创建漂亮的脚本方案——要么你不得不用 JavaScript 创建大量的标记;要么你的网页要依靠 JavaScript 。.
HTML 代码中需要考虑下面问题:钩子和关系。
HTML 钩子
最重要的 HTML 钩子是 ID,因为可通过最快的 DOM 方法 getElementById
获取标记元素。合法的 HTML 文档中 ID 是唯一的 ( IE 中有一个关于 name 和 ID 的 bug,但好的库都能绕开此 bug ),所以在其中使用 ID 和安全的。
其他钩子是可使用 getElementsByTagName
获取的 HTML 元素和 CSS class——大部分浏览器中没有原生 DOM 方法可用于获取 CSS class ( Mozilla 以后将有,Opera 9.5 已经包含);但有很多库提供 getElementsByClassName
以访问 CSS class。
HTML 关系
HTML 另一个很有趣的地方时标记间的关系。请问问自己下面问题:
- 如何最快最简单的遍历 DOM 并获得所需元素?
- 当需要改变多个子元素时,应该改变哪个元素?
- 元素的哪些属性或信息可以帮助访问其他元素?
遍历 DOM 树是较慢的操作,这就是为什么最好采用已在浏览器中被使用的技术。
3. 将遍历留给专家 ( CSS, 更快的遍历 DOM)
DOM 提供了遍历 DOM 的方法和属性 ( getElementsByTagName
, nextSibling
, previousSibling
, parentNode
等),这些属性常常让人迷惑。有趣的是我们已有达到此目的的技术: CSS。
CSS 通过 CSS 选择器遍历 DOM 以访问所需的元素并改变其视觉属性。可用简单的 CSS 选择器取代复杂的 JavaScript DOM 函数:
var n = document.getElementById('nav');
if(n){
var as = n.getElementsByTagName('a');
if(as.length > 0){
for(var i=0;as[i];i++){
as[i].style.color = '#369';
as[i].style.textDecoration = 'none';
}
}
}
/* is the same as */
#nav a{
color:#369;
text-decoration:none
}
CSS 是强大的可以加以利用的技术。可以动态地给高层的 DOM 元素添加 class,或者修改 ID 。如使用 DOM 给 body 添加 class,那网页设计者可以轻松地定义网页的静态和动态版本:
JavaScript:
var dynamicClass = 'js';
var b = document.body;
b.className = b.className ? b.className + ' js' : 'js';
CSS:
/* static version */
#nav {
....
}
/* dynamic version */
body.js #nav {
....
}
4. 理解浏览器和用户 (在既有的已被证明有效的模式基础上创建你所需要的)
Unobtrusive JavaScript 的一个重要部分就是理解浏览器如何工作 (特别理解为何浏览器会不能正常工作),并理解用户期望是什么。开发者很容易轻率的使用 JavaScript 创建完全不同的界面:可拖拽界面、可折叠区域、滚动条,这些都可以通过 JavaScript 实现,但这不仅仅是技术问题。你应该问问你自己:
- 我的新用户界面独立于输入设备么?如果答案是否,那备用方案是什么?
- 新界面是否符合浏览器规则或富界面( richer interface)规则?(你能使用光标在多级菜单中导航么?还是需要使用 tab ?)
- 什么功能是需要提供的,但又依赖于 JavaScript 的?
最后一项不是问题,在需要时可以使用 DOM 创建 HTML 。一个很好的例子就是"打印此页"链接 —— 浏览器没有提供非 JavaScript 方式打印网页,所以你需要使用DOM创建此类链接。同样的,可点击并可展开/收缩其内容的标题也需要使用 DOM 创建。无法使用键盘激活标题,但可以激活链接。为了创建可点击的标题,你应该使用JavaScript为其添加链接 —— 这样即使是键盘用户也可以展开/收缩内容。
解决此类问题的很好的资源就是设计模式库。知道浏览器中什么特性独立与输入设备是一个经验问题。首先需要理解事件处理概念。
5. 理解事件 (Event handling to initiate change)
事件处理是 Unobtrusive JavaScript 的重要一环。重点不是让所有元素都可以点击并拖拽或添加内联处理;重点是理解事件处理是真正的分离。我们分离了 HTML, CSS 和 JavaScript ,但有了事件处理可更进一步。
网页中的元素等待处理器监听状态变化。变化发生后,处理器获得“神奇的”对象 (通常是名为e
的参数),此对象会告知哪些元素发生了哪些事件,及如何应变。
事件处理很酷的一点是事件不仅发生在一个元素中,事件会达到 DOM 树中所有的祖先元素 (所有事件都有此特性,但 focus 和 blur 除外)。这允许你为多个元素设置同一个事件处理器(如为导航列表),使用事件处理器方法即可获取真正触发事件的元素。此技巧被称作事件委托( event delegation ),其优点如下:
- 你只需测试一个元素是否存在,不必测试所有元素
- 可以动态添加或删除子元素,而不需要移除或添加相应新事件处理器
- 可以响应不同元素的同一个事件
另外可以停止事件向父节点传播,也可以重载 HTML 元素(如链接)的默认行为。但有时这不是一个好主意,浏览器这么做是有原因的。比如,指向页内目标的链接,允许它们被跟随可以确保用户可将脚本状态也加入书签。
6. 与别人合作 (命名空间、作用域和模式)
你的代码不可能只用于这一个网页。因此需要确保你的代码中没有全局函数且没有变量名称不会和其他脚本冲突。有很多方案可以保证这一点。最简单的方法就是为每一个变量初始化使用var
关键字。假设有以下代码:
var nav = document.getElementById('nav');
function init(){
// do stuff
}
function show(){
// do stuff
}
function reset(){
// do stuff
}
上面代码有一个全局变量 nav
和三个全局函数 init
, show
及 reset
。这三个函数都可以使用 nav 变量及另两个函数:
var nav = document.getElementById('nav');
function init(){
show();
if(nav.className === 'show'){
reset();
}
// do stuff
}
function show(){
var c = nav.className;
// do stuff
}
function reset(){
// do stuff
}
可通过将上面代码封装到对象中避免污染全局命名空间,这样函数就变成了对象方法,变量变成了对象属性。在变量名称或方法名称后使用冒号,并在定义之间使用逗号分隔。
var myScript = {
nav:documentgetElementById('nav'),
init:function(){
// do stuff
},
show:function(){
// do stuff
},
reset:function(){
// do stuff
}
}
在对象内外都可以通过使用对象名加'.'来使用对象属性或者对象方法。
var myScript = {
nav:documentgetElementById('nav'),
init:function){
myScript.show();
if(myScript.nav.className === 'show'){
myScript.reset();
}
// do stuff
},
show:function){
var c = myScript.nav.className;
// do stuff
},
reset:function){
// do stuff
}
}
这种方法的缺点是每当需要使用变量或方法时都需要重复输入对象名,且对象中所有变量和方法都是可以公开访问的。如果你想让部分脚本成为公开访问的呢?可以使用模块模式:
var myScript = function(){
// these are all private methods and properties
var nav = document.getElementById('nav');
function init(){
// do stuff
}
function show(){
// do stuff
}
function reset(){
// do stuff
}
// public methods and properties wrapped in a return
// statement and using the object literal
return {
public:function(){
},
foo:'bar'
}
}();
可以公开访问的属性和方法被封住进 return 语句。上例中 myScript.public()
和 myScript.foo
可以在外部使用。烦人的是如果你想在另一个公开方法或者私有方法中使用某个公开方法,你必须要使用很长的名称 (主对象名可能会很长)。为克服此缺点,将其定义为私有方法并返回别名:
var myScript = function(){
// these are all private methods and properties
var nav = document.getElementById('nav');
function init(){
// do stuff
}
function show(){
// do stuff
// do stuff
}
function reset(){
// do stuff
}
var foo = 'bar';
function public(){
}
// return public pointers to the private methods and
// properties you want to reveal
return {
public: public,
foo:foo
}
}();
这保持了代码风格的一致性并可使用短一些的名称访问。
如果你不想让外面代码调用函数,可将代码封装在匿名函数中并在定义后立即使用:
(function(){
// these are all private methods and properties
var nav = document.getElementById('nav');
function init(){
// do stuff
show(); // no need for prepended object name
}
function show(){
// do stuff
}
function reset(){
// do stuff
}
})();
这对那些只执行一遍且不依赖其他函数的函数来说是一个很好的模式。
遵循上述规则可以让你的代码更好的为用户服务、并更好的在机器中与其他人的代码相互配合工作。但此外你还需要考虑另一群人。
7. 为以后的开发者着想 (简轻代码维护工作)
编写Unobtrusive JavaScript的最后一个环节是在开发结束后重新审视代码,并为脚本上线后接手此代码的下一位开发者着想:
- 每一个变量名和函数名都符合逻辑且易懂么?
- 代码结构如何?你能顺畅的从头读到尾么?
- 所有的依赖性都显而易见么?
- 在那些容易引起疑惑的地方你添加注释了么?
需要牢记的是 HTML 和 CSS 相比JavaScript来说更可能被改变 (因为他们构成了视觉输出)。因此不要让 class 、 ID 名称及要显示给用户的字符串被深深“埋藏”在你的代码中,最好将其统一放置在 config 对象中。
myscript = function(){
var config = {
navigationID:'nav',
visibleClass:'show'
};
var nav = document.getElementById(config.navigationID);
function init(){
show();
if(nav.className === config.visibleClass){
reset();
};
// do stuff
};
function show(){
var c = nav.className;
// do stuff
};
function reset(){
// do stuff
};
}();
这样代码维护人员可知道去何处修改代码,且不必改动其他代码。