# 案例研究：图片库改进版

在[最佳实践](http://zlw.poker/2016/11/02/%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5/)学到如何平稳退化、分离JavaScript、向后兼容和提高性能，在这里，将运用这些知识来改进图片库。

### 快速回顾

在[案例研究：JavaScript图片库](http://zlw.poker/2016/11/01/%E6%A1%88%E4%BE%8B%E7%A0%94%E7%A9%B6%EF%BC%9AJavaScript%E5%9B%BE%E7%89%87%E5%BA%93/)里，我编写了一个用来替换“占位符”图片的src属性的脚本，只用一个网页就建立起了图片库。

### 它支持平稳退化吗

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

```html
<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属性：

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

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

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

#### 添加事件处理函数

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

* 检查当前浏览器是否理解getElementsByTagName和getElementById；
* 检查当前网页是否存在一个id为imagegallery的元素；（这项测试是一个预防性措施。现在我知道调用这个JavaScript函数的文档里有一个id属性值等于imagegallery的列表清单元素，但我不确定这在将来会不会发生变化。有了这个预防性措施，即使以后从网页上删掉图片库，也用不着这个网页的JavaScript代码会突然出错。作为一条原则，**如果想用JavaScript给某个网页添加一些行为，就不应该让JavaScript代码对这个网页的结构有任何依赖。**）
* 遍历imagegallery元素中的所有连接；
* 设置onclick事件，让它在有关链接被点击时完成以下操作： 1.把这个链接作为参数传递给showPic函数； 2.取消链接被点击时的默认行为，不让浏览器打开这个链接。

```javascript
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函数绑定到这个事件上：

```javascript
window.onload=prepareGallery;
```

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

```javascript
window.onload=firstFunction;
window.onload=secondFunction;
```

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

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

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

* 把现有的window\.onload事件处理函数的值存入变量oldonload。
* 如果在这个处理函数上还没有绑定任何函数，就像平时那样把新函数添加给它。
* 如果在这个处理函数上已经绑定了一些函数，就把新函数追加到现有指令的末尾。

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

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

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

```javascript
addloadEvent(firstFunction);
addloadEvent(secondFunction);
```

### 不要做太多假设

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

```javascript
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的返回值进行取反：

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

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

```javascript
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属性：

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

检查方法：

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

if(whichpic.getAttribute("title"))
```

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

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


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

其他检查：

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

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

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

```javascript
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代码留下了一个“挂钩”：

```html
<ul id="imagegallery">
```

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

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

```html
<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对象。这个对象可以把下面这样的语句：

```javascript
document.getElementsByTagName("form")
```

简化为：

```javascript
document.forms
```

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

```javascript
element.getAttribute("src")
```

简化为：

```javascript
element.src
```

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

### 效果预览

![Snapshots](http://ofolh8dcq.bkt.clouddn.com/snapshots.PNG)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://lewiszlw.gitbook.io/notebooks/blogs/history/an-li-yan-jiu-tu-pian-ku-gai-jin-ban.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
