案例研究:图片库改进版

最佳实践学到如何平稳退化、分离JavaScript、向后兼容和提高性能,在这里,将运用这些知识来改进图片库。

快速回顾

案例研究:JavaScript图片库里,我编写了一个用来替换“占位符”图片的src属性的脚本,只用一个网页就建立起了图片库。

它支持平稳退化吗

第一个问题是:“如果JavaScript功能被禁用,会怎样?” 分析可知:即使JavaScript功能被禁用,用户也可以浏览图片库里的所有内容,网页里的所有连接也都可以正常工作:

<li>
  <a href="images/fireworks.jpg" onclick="showPic(this);return false;" title="a fireworks display">fireworks</a>
</li>

在JavaScript功能被禁用的情况下,浏览器将沿着href属性给出的链接前进,用户将看到一张图片而不是“该页无法显示”之类的错误信息。

它的JavaScript与HTML标记是分离的吗

由代码知显然不是分离的。 把JavaScript代码移出HTML文档不是难事,但为了让浏览器知道页面里都有哪些链接有着不一样的行为,我们必须找到一种“挂钩”把JavaScript代码与HTML文档中的有关标记关联起来。有多种办法可以做到这点: 可以给每个链接分别添加一个class属性:

<li>
  <a href="images/fireworks.jpg" class="gallerypic" title="a fireworks display">fireworks</a>
</li>

但这种方法不够理想,这与给它们分别添加事件处理函数同样麻烦。 第二种办法:图片清单里的每个链接有一个共同点:它们都包含在同一个列表清单元素里。给整个清单设置一个独一无二的id的办法要简单得多:

<ul id="imagegallery">
  <li>...</li>
</ul>

添加事件处理函数

现在,需要编写一个简短的函数把有关操作关联到onclick事件上。我将其命名为prepareGallery。 下面是这个函数需要完成的工作:

  • 检查当前浏览器是否理解getElementsByTagName和getElementById;

  • 检查当前网页是否存在一个id为imagegallery的元素;(这项测试是一个预防性措施。现在我知道调用这个JavaScript函数的文档里有一个id属性值等于imagegallery的列表清单元素,但我不确定这在将来会不会发生变化。有了这个预防性措施,即使以后从网页上删掉图片库,也用不着这个网页的JavaScript代码会突然出错。作为一条原则,如果想用JavaScript给某个网页添加一些行为,就不应该让JavaScript代码对这个网页的结构有任何依赖。

  • 遍历imagegallery元素中的所有连接;

  • 设置onclick事件,让它在有关链接被点击时完成以下操作: 1.把这个链接作为参数传递给showPic函数; 2.取消链接被点击时的默认行为,不让浏览器打开这个链接。

function prepareGallery(){
	if(!document.getElementsByTagName) return false;
	if(!document.getElementById) return false;
	if(!document.getElementById("imagegallery")) return false;
	var gallery=document.getElementById("imagegallery");
	var links=gallery.getElementsByTagName("a");
	for(var i=0;i<links.length;i++){
			links[i].onclick=function(){       //定义了一个匿名函数。这是一种在代码执行时创建函数的办法。
					showPic(this);
					return false;
				}
		}
}

调用此函数,就会把onclick事件绑定到“id”等于“imagegallery”的元素内的各个链接元素上。

共享onload事件

必须执行prepareGallery函数才能对onclick事件进行绑定。 如果马上执行这个函数,它就无法完成其工作。在HTML文档完成加载之前执行脚本,此时DOM是不完整的。 应该让这个函数在网页加载完毕后立刻执行。网页加载完毕时会触发一个onload事件,这个事件与window对象相关联。所以我们须把prepareGallery函数绑定到这个事件上:

window.onload=prepareGallery;

但如果有多个函数,如果像下面这样逐一绑定,它们中只有最后那个函数才会得到实际执行

window.onload=firstFunction;
window.onload=secondFunction;

有种办法可以解决这个问题:可以先创建一个匿名函数来容纳这两个函数,然后把那个匿名函数绑定到onload事件上:

window.onload=function(){
		firstFunction();
		secondFunction();
	}

这里还有一个弹性最佳的解决方案——使用addloadEvent函数。它只有一个参数:打算在页面加载完毕时执行的函数的名字。 下面是addloadEvent函数将要完成的操作:

  • 把现有的window.onload事件处理函数的值存入变量oldonload。

  • 如果在这个处理函数上还没有绑定任何函数,就像平时那样把新函数添加给它。

  • 如果在这个处理函数上已经绑定了一些函数,就把新函数追加到现有指令的末尾。

下面是addloadEvent函数的代码清单:

function addloadEvent(func){
		var oldonload=window.onload;
		if(typeof window.onload!='function'){
				window.onload=func;
			}else{
					window.onload=function(){
							oldonload();
							func();
						}
				}
	}

这将把那些在页面加载完毕时执行的函数创建为一个队列。如果想把刚才那两个函数添加到这个队列里去,只需要写出以下代码就行了:

addloadEvent(firstFunction);
addloadEvent(secondFunction);

不要做太多假设

在showPic函数里发现的一个问题是,没有让它进行任何测试和检查。 showPic函数将由prepareGallery函数调用,而我已经在后者的开头对getElementById和getElementsByTagName等DOM方法是否存在进行过检查,所以我确切地知道用户的浏览器不会因为不理解这个方法而出问题。不过并未对id值等于placeholder和description的元素进行任何检查。

function showPic(whichpic){
	if(!document.getElementById("placeholder")) return false;
	var source = whichpic.getAttribute("href");
	var placeholder = document.getElementById("placeholder");
	placeholder.setAttribute("src",source);

	if(document.getElementById("description")){	
	var text=whichpic.getAttribute("title");
	var description=document.getElementById("description");
	description.firstChild.nodeValue=text;
	}
	return true;                 //即使description元素不存在,切换显示新图片的操作也照常进行
}

改进后的showPic()函数不再假设有关标记文档里肯定存在着placeholder图片和description元素。即使文档里没有placeholder图片,也不会发生任何JavaScript错误。 可是还有一个问题:如果把placeholder图片从标记文档里删除并在浏览器里刷新这页面,就会出现这种情况,无论点击imagegallery清单里的哪个链接,都没有任何反应。这意味着我们的脚本不能平稳退化。问题在于prepareGallery函数做出了这样一个假设:showPic函数肯定会正常返回。基于这一假设,prepareGallery函数取消了onclick事件的默认行为。

showPic函数应该返回两个可能的值:

  • 如果图片切换成功,返回true。

  • 如果图片切换不成功,返回false。

为修正这个问题,应该在返回前验证showPic的返回值,以便决定是否阻止默认行为。showPic返回true,那么更新placeholder。在onclick事件处理函数中,我们可以利用“!”对showPic的返回值进行取反:

links[i].onclick=function(){
		return !showPic(this);
	}

现在,如果showPic返回true,我们就返回false,浏览器不会打开那个链接。如果showPic返回false,那么我们认为图片没有更新,于是返回true以允许默认行为发生。下面是prepareGallery函数现在的代码清单:

function prepareGallery(){
	if(!document.getElementsByTagName) return false;
	if(!document.getElementById) return false;
	if(!document.getElementById("imagegallery")) return false;
	var gallery=document.getElementById("imagegallery");
	var links=gallery.getElementsByTagName("a");
	for(var i=0;i<links.length;i++){
			links[i].onclick=function(){       
					return !showPic(this);
				}
		}
}

优化

这个函数已经很完善了。尽管如此,在showPic函数里仍存在一些需要处理的假设。 例如,假设每个链接都有一个title属性:

var text=whichpic.getAttribute("title");

检查方法:

if(whichpic.getAttribute("title")!=null)

if(whichpic.getAttribute("title"))

在title属性不存在时把text的值设置为空字符串:

if(whichpic.getAttribute("title")){
		var text=whichpic.getAttribute("title");
	}else{
			var text="";
		}


var text=whichpic.getAttribute("title") ? whichpic.getAttribute("title") : "";    //另一种方法

其他检查:

if(placeholder.nodeName!="IMG") return false;  //检查placeholder。nodeName返回一个大写字母的值。

if(description.firstChild.nodeType==3){
		description.firstChild.nodeValue=text;
	}

最终prepareGallery()和showPic()代码清单:

function prepareGallery(){
	if(!document.getElementsByTagName) return false;
	if(!document.getElementById) return false;
	if(!document.getElementById("imagegallery")) return false;
	var gallery=document.getElementById("imagegallery");
	var links=gallery.getElementsByTagName("a");
	for(var i=0;i<links.length;i++){
			links[i].onclick=function(){
					return showPic(this)?false:true;
				}
		}
}
function showPic(whichpic){
	if(!document.getElementById("placeholder")) return false;
	var source = whichpic.getAttribute("href");
	var placeholder = document.getElementById("placeholder");
	if(placeholder.nodeName!="IMG") return false;
	placeholder.setAttribute("src",source);

	if(document.getElementById("description")){	
		var text=whichpic.getAttribute("title")?whichpic.getAttribute("title"):""; 
		var description=document.getElementById("description");
		description.firstChild.nodeValue=text;
	}
	return true;               
}

把JavaScript与CSS结合起来

把JavaScript代码从HTML文档里分离出去还带来另一个好处。在把内嵌型事件处理函数移出标记文档时,我在文档里为JavaScript代码留下了一个“挂钩”:

<ul id="imagegallery">

这个挂钩完全可以用在CSS样式表里。

还可以把图片链接换成缩略图:

<li>
  <a href="images/fireworks.jpg" title="a fireworks display">
    <img src="images/thumbnail_fireworks.jpg" alt="fireworks" />
  </a>
</li>

DOM Core和HTML-DOM

至此,在编写JavaScript代码时只用到了一下几个DOM方法:

  • getElementById

  • getElementsByTagName

  • getAttribute

  • setAttribute

这些方法都是DOM Core的组成部分。它们并不专属于JavaScript,支持DOM的任何一种程序设计语言都可以使用它们。它们的用途也并非仅限于处理网页,它们可以用来处理用任何一种标记语言(比如XML)编写出来的文档。 在使用JavaScript语言和DOM为HTML文件编写脚本时,还有许多属性可供选择。例如属性onclick,用于图片库的事件管理。这些属性属于HTML-DOM,它们在DOM Core出现之前很久就已经为人们所熟悉了。 比如说,HTML-DOM提供了一个forms对象。这个对象可以把下面这样的语句:

document.getElementsByTagName("form")

简化为:

document.forms

类似地,HTML-DOM还提供了许多描述各种HTML元素的属性。比如说,HTML-DOM图片提供的src属性可以把下面这样的语句:

element.getAttribute("src")

简化为:

element.src

这些方法和属性可以相互替换。同样的操作既可以只使用DOM Core来实现,也可以使用HTML-DOM来实现。通常HTML-DOM代码更短,必须提醒一点的是,它们只能用来处理web文档。

效果预览

Last updated