# 动态创建标记

此前见过的绝大多数DOM方法只能用来查找元素。getElementById和getElementsByTagName都可以方便快捷地找到文档中的某个或某些特定的元素节点，这些元素随后可以用诸如setAttribute和nodeValue之类的方法和属性来处理。在这两种情况里，都是对已经存在的元素做出修改。这是绝大多数JavaScript函数的工作原理。网页的结构有标记负责，JavaScript函数只是来改变某些细节而不改变其底层结构。不过JavaScript也可以通过创建新元素和修改现有元素来改变网页结构。

## 一些传统方法

### document.write

document对象的write()方法可以方便快捷地把字符串插入到文档里。 示例：

```html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8"/>
  <title>Test</title>
</head>
<body>
  <script>
    document.write("<p>This is inserted.</p>");
  </script>
</body>
</html>
```

将这个html文件加载到web浏览器，你将看到“This is inserted.”的文本段落。 document.write的最大缺点是它违背了“行为应该与表现分离”的原则。从某种意义上讲，使用document.write方法有点像使用\<font>标签去设定字体和颜色。虽然这两种技术在HTML文档里都工作得不错，但它们不够优雅。把结构、行为和样式分开永远是一个好主意。只要有可能，就应该用外部CSS文件代替\<font>标签去设定和管理网页的样式信息，最好用外部JavaScript文件去控制网页的行为。应该避免在\<body>部分乱用\<script>标签，避免使用document.write方法。

### innerHTML属性

innerHTML属性可以用来读、写某给定元素里的**HTML内容**。 例如下面一段代码：

```html
<div id="testdiv">
  <p>This is <em>my</em> content.</p>
</div>
```

用DOM的眼睛看testdiv内的标记结构： ![testdiv结构](http://ofolh8dcq.bkt.clouddn.com/testdiv.PNG) 使用DOM提供的方法和属性可以对任何一个节点进行单独的访问。但从innerHTML属性的角度看，id为testdiv的标记里面只有一个值为\<p>This is \<em>my\</em> content.\</p>的HTML字符串。 ![innerHTML结构](http://ofolh8dcq.bkt.clouddn.com/innerHTML%E7%BB%93%E6%9E%84.PNG) 很明显，innerHTML属性无细节可言。要想获得细节，就必须使用DOM方法和属性。 innerHTML属性不仅可以读出元素的HTML内容，还可以把HTML内容写入元素。编辑文件，让id属性值为testdiv的元素变成空白：

```html
<div id="testdiv">
</div>
```

把下面这段JavaScript代码放入.js文件，就可以把一段HTML内容插入这个\<div>标签：

```javascript
window.onload=function(){
		var testdiv=document.getElementById("testdiv");
		testdiv.innerHTML="<p>I inserted <em>this</em> content.</p>";
	}
```

testdiv元素里有没有HTML内容无关紧要：**一旦你使用了innerHTML属性，它的内容就将全部替换。** 在需要把一大段HTML内容插入一份文档时，innerHTML属性可以让你又快又简单地完成这一任务。不过，innerHTML属性不会返回任何对刚插入内容的引用。如果想对那些内容进行处理，则需要使用DOM提供的那些精确的方法和属性。

## DOM方法

DOM是文档的表示。DOM所包含的信息与文档里的信息一一对应。它是一条双向车道，不仅可以获取文档的内容，还可以更新文档的内容。例如setAttribute方法，用这个方法可以改变文档的某个属性节点，不过setAttribute方法并未改变文档的物理内容，如果用文本编辑器打开，我们将看不到任何变化，这是因为浏览器实际显示的是那颗DOM节点树。在浏览器看来，DOM节点树才是文档 一旦明白这个，以动态方式实时创建标记就不那么难理解了。你并不是在创建标记，而是在改变DOM节点树。

### createElement方法

还是让id等于testdiv的那个\<div>标签内容变成空白：

```html
<div id="testdiv">
</div>
```

我想把一段文本插入testdiv元素。这项任务需分两个步骤完成：

* 创建一个新的元素
* 把这个新元素插入节点树

第一个步骤要用DOM方法createElement来完成。

```javascript
document.createElement(nodeName)
```

### appendChild方法

appendChild方法是把创建的节点变成某个节点的子节点。

```javascript
parent.appendChild(child)
```

创建一个p元素然后将其变成testdiv元素的一个子节点：

```javascript
var para=document.createElement("p");
var testdiv=document.getElementById("testdiv");
testdiv.appendChild(para);
```

### createTextNode方法

你已经创建出了一个元素节点并把它插入了文档的节点树，这个节点是一个空白的p元素。你想把一些文本放入这个p元素，但createElement方法帮不上忙，它只能创建元素节点。而你需要创建一个文本节点，此时可以用createTextNode方法来实现它。

```javascript
document.createTextNode(text)
```

最终代码：

```javascript
window.onload=function(){
		var para=document.createElement("p");
		var testdiv=document.getElementById("testdiv");
		testdiv.appendChild(para);
		var txt=document.createTextNode("hello world");
		para.appendChild(txt);
	}
```

或者换种思路，先将文本节点追加到p元素节点，然后再把p元素节点追加到文档中的testdiv元素节点：

```javascript
window.onload=function(){
		var para=document.createElement("p");
		var txt=document.createTextNode("hello world");
		para.appendChild(txt);

		var testdiv=document.getElementById("testdiv");
		testdiv.appendChild(para);
	}
```

## 重回图片库

回顾[案例研究：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/)gallery.html文件中的标记（可以将链接文字改为缩略图）。这个文件有一个图片（placeholder）和一段文字（description）仅仅是位showPic脚本服务的。既然这些元素的存在只是为了让DOM方法处理它们，那么用DOM方法来创建它们才是最合适的选择。 先把这些元素从gallery.html中删除。然后编写一个函数preparePlaceholder并把它放进showPic.js文件，然后在文档加载时调用这个函数。下面是这个函数要完成的任务： 1.创建一个img元素节点 2.设置这个节点的id属性 3.设置这个节点的src属性 4.设置这个节点的alt属性 5.创建一个p元素节点 6.设置这个节点的id属性 7.创建一个文本节点 8.把这个文本节点追加到p元素上 9.把p元素和img元素插入到gallery.html文档

创建这些元素和设置各有关属性的工作：

```javascript
var placeholder=document.createElement("img");
placeholder.setAttribute("id","placeholder");
placeholder.setAttribute("src","images/placeholder.jpg");
placeholder.setAttribute("alt","my image gallery");
var description=document.createElement("p");
description.setAttribute("id","description");
var desctxt=document.createTextNode("choose an image");
```

把创建的文本节点插入p元素：

```javascript
description.appendChild(desctxt)
```

最后是把新创建的元素插入文档：

```javascript
document.getElementsByTagName("body")[0].appendChild(placeholder);
document.getElementsByTagName("body")[0].appendChild(description);

//也可以使用HTML-DOM提供的属性body
document.body.appendChild(placeholder);
document.body.appendChild(description);
```

以上代码工作得很好，但这一切都依赖于一个细节：图片清单（\<ul>...\</ul>）刚好是\<body>部分的最后一个元素。但如果这个图片清单的后面还要一些其他的内容，而我们想把新创建的元素紧跟在图片清单后面怎么办呢？

### 在已有元素前插入一个新元素

DOM提供了名为insertBefore()方法，这个方法将把一个新元素插入到一个现有元素的前面。

```javascript
parentElement.insertBefore(newElement,targetElement)
```

* 新元素：你想插入的元素（newElement）
* 目标元素：你想把这个新元素插入到哪个元素（targetElement）之前
* 父元素：目标元素的父元素（parentElement）

\*\*我们不必搞清楚父元素到底是哪个，因为targetElement元素的parentNode属性值就是它。\*\*在DOM里，元素节点的父元素必须是另一个元素节点（属性节点和文本节点的子元素不允许是元素节点）。

### 在现有元素后插入一个新元素

DOM没有提供这样一个函数。

#### 编写insertAfter函数

虽然DOM本身没有提供insertAfter方法，但它确实提供了把一个节点插入到另一个节点之后所需的所有工具。我们可以利用已有的DOM方法和属性编写一个insertAfter函数：

```javascript
function insertAfter(newElement,targetElement){
		var parent=targetElement.parentNode;
		if(parent.lastChild==targetElement){
				parent.appendChild(newElement);
			}else{
					parent.insertBefore(newElement,targetElement.nextSibling);
				}
	}
```

targetElement.nextSibling是目标元素的下一个兄弟元素。

#### 使用insertAfter函数

使用insertAfter函数后preparePlaceholder函数现在的样子：（记住测试浏览器是否支持这些DOM方法）

```javascript
function preparePlaceholder(){
		if(!document.createElement) return false;
		if(!document.createTextNode) return false;
		if(!document.getElementById) return false;
		if(!document.getElementById("imagegallery")) return false;
		var placeholder=document.createElement("img");
		placeholder.setAttribute("id","placeholder");
		placeholder.setAttribute("src","images/placeholder.jpg");
		placeholder.setAttribute("alt","my image gallery");
		var description=document.createElement("p");
		description.setAttribute("id","description");
		var desctxt=document.createTextNode("choose an image");
		description.appendChild(desctxt);
		var gallery=document.getElementById("imagegallery");
		insertAfter(placeholder,gallery);
		insertAfter(description,placeholder);
	}
```

### 图片库二次改进版

现在showPic.js文件包含5个不同的函数，分别是：

* addLoadEvent函数
* insertAfter函数
* preparePlaceholder函数
* prepareGallery函数
* showPic函数

preparePlaceholder函数负责创建一个img元素和一个p元素。这个函数将把这些新创建的元素插入到节点树里图片库清单的后面。prepareGallery函数负责处理事件。这个函数将遍历处理图片库清单里的每个链接。当用户点击这些链接中的某一个时，就会调用showPic函数。showPic函数负责把“占位符图片”切换为目标图片。 下面是最终完成的showPic.js文件：

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

function insertAfter(newElement,targetElement){
		var parent=targetElement.parentNode;
		if(parent.lastChild==targetElement){
						parent.appendChild(newElement);
				}else{
						parent.insertBefore(newElement,targetElement.nextSibling);
					}
	}

function preparePlaceholder(){
		if(!document.createElement) return false;
		if(!document.createTextNode) return false;
		if(!document.getElementById) return false;
		if(!document.getElementById("imagegallery")) return false;
		var placeholder=document.createElement("img");
		placeholder.setAttribute("id","placeholder");
		placeholder.setAttribute("src","images/placeholder.jpg");
		placeholder.setAttribute("alt","my image gallery");
		var description=document.createElement("p");
		description.setAttribute("id","description");
		var desctxt=document.createTextNode("choose an image");
		description.appendChild(desctxt);
		var gallery=document.getElementById("imagegallery");
		insertAfter(placeholder,gallery);
		insertAfter(description,placeholder);
	}

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;               
}

addloadEvent(preparePlaceholder);
addloadEvent(prepareGallery);
```

JavaScript代码增加了，但标记相应的减少了。gallery.html文件现在只包含一个由JavaScript脚本和CSS样式表共用的“挂钩”。这个“挂钩”就是图片清单的id属性。

```html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8"/>
  <title>Image Gallery</title>
  <link rel="stylesheet" href="styles/layout.css" media="screen"/>
</head>
<body>
  <h1>Snapshots</h1>
  <ul id="imagegallery">
    <li>
    <a href="images/fireworks.jpg" title="a firework display">fireworks</a>
    </li>
    <li>
    <a href="images/coffee.png" title="a cup of coffee">coffee</a>
    </li>
    <li>
    <a href="images/rose.jpg" title="a red rose">rose</a>
    </li>
    <li>
    <a href="images/bigben.jpeg" title="the famouse clock">bigben</a>
    </li>
  </ul>
  <script src="scripts/showPic.js"></script>
</body>
</html>
```

## Ajax

以前，web应用都要涉及大量的页面刷新：用户点击了某个链接，请求发送回服务器，然后服务器根据用户的操作再返回新页面。即便用户看到的只是页面的一小部分发生变化，也要刷新和重新加载整个页面。使用Ajax就可以做到只更新页面中的一小部分。 Ajax的主要优势就是对页面的请求以异步方式发送到服务器，而服务器不会用整个页面来响应请求它会在后台处理，与此同时用户还能继续浏览页面并与页面交互。你的脚本则可以按需加载和创建页面内容，而不会打断用户的浏览体验。

### XMLHttpRequest对象

Ajax技术的核心就是XMLHttpRequest对象。这个对象充当着浏览器中的脚本（客户端）与服务器之间的中间人的角色。以往的请求都由浏览器发出，而JavaScript通过这个对象可以自己发出请求，同时也自己处理响应。 创建XMLHttpRequest对象：微软最早在IE5中以ActiveX对象的形式实现了一个名叫XMLHTTP的对象。在IE中创建新的对象要使用以下代码：

```javascript
var request=new ActiveXObject("Msxml2.XMLHTTP.3.0");
```

更麻烦的是不同IE版本中使用的XMLHTTP对象也不完全相同。 其他浏览器则基于XMLHttpRequest来创建新对象：

```javascript
var request=new XMLHttpRequest();
```

为了兼容所有浏览器，我们要写个函数getHTTPObject()达到目的：

```javascript
function getHTTPObject(){
	if (typeof XMLHttpRequest=="undefined")
		XMLHttpRequest=function(){
			try {return new ActiveXObject("Msxml2.XMLHTTP.6.0");}
				catch(e){}
			try{return new ActiveXObject("Msxml2.XMLHTTP3.0");}
				catch(e){}
			try{return new ActiveXObject("Msxml2.XMLHTTP");}
				catch(e){}
			return false;
		}
		return new XMLHttpRequest();
}
```

getHTTPObject通过对象检测技术使其能在不同浏览器实现兼容。用getHTTPObject函数创建XMLHttpRequest对象：

```javascript
var request=getHTTPObject();
```

XMLHttpRequest对象有许多方法。其中最有用的是open方法，它用来**指定**服务器上将要访问的文件，指定请求的类型有：GET、POST和SEND。 例如：

```javascript
function getNewContent(){
	var request=getHTTPObject();
	if(request){
		request.open("GET","example.txt",true);   //第三个参数用于指定请求是否以异步方式发送和处理
		request.onreadystatechange=function(){
		if(request.readyState==4){
			var para=document.createElement("p");
			var txt=document.createTextNode(request.responseText);
			para.appendChild(txt);
			document.getElementById('new').appendChild(para);
			}
		};
		request.send(null);
	}else{
			alert('Sorry,your browser does not support XMLHttpRequest');
		}
}
```

当页面加载完成后，以上代码会发起一个GET请求，请求与.html文件位于同一目录的example.txt文件。代码中onreadystatechange是一个事件处理函数，它会在服务器给XMLHttpRequest对象送回响应的时候被触发执行。在指定了请求的目标，也明确了如何响应之后，就可以用send方法来发送请求了：request.send(null);不发送任何数据的请求。 服务器在向XMLHttpRequest对象发回响应时，该对象有许多属性可用，浏览器会在不同阶段更新readyState属性的值：

* 0：未初始化
* 1：正在加载
* 2：加载完毕
* 3：正在交互
* 4：完成

只要readyState属性的值变成了4，就可以访问服务器发送回来的数据了。访问服务器发送回来的数据要通过两个属性完成。一个是responseText属性，用于保存文本字符串形式的数据。另一个是responseXML属性，用于保存Content-Type头部中指定为“text/xml”的数据，其实是一个DocumentFragment对象。

> 注意：在使用Ajax时，千万要注意同源策略。使用XMLHttpRequest对象发送的请求只能访问与其所在的HTML处于同一个域中的数据，不能向其他域发送请求。此外，有些浏览器还会限制Ajax请求使用的协议，比如Chrome。

异步请求有一个容易被忽略的问题是异步性，就是脚本在发送XMLHttpRequest请求之后，仍然会继续执行，不会等待响应返回。为此，如果其他脚本依赖于服务器的响应，那么就得把相应的代码都转移到指定给onreadystatechange属性的那个函数中。

### 渐进增强与Ajax

构建Ajax网站的最好方法，也是先构建一个常规的网站，然后Hijax它。

### Hijax

Hijax是指渐进增强地使用Ajax。
