前几章节主要是用DOM方法来查找元素,查找或设置节点属性。除了查找节点和设置属性,DOM还有一些方法能够创建元素、修改元素,利用这些方法,能够动态改变网页。举个例子来说,你可能在某些时候看到一个网页,这个网页简洁实用,你想看看它的源码实现,通过浏览器右键菜单查看到网页源码却让你很意外,你发现网页的源码很简单,但和页面不是一一对应,有些东西在源码中找不到,这可能就是因为这个网页是动态构建的。
一些传统方法
除了通过DOM方法来往网页中写入一些元素之外,还有一些过去经常用的技术。第一个是document.write( ),这个函数可能经常在调试或者临时代码中用到,可以直接把HTML代码插入到网页中,代码如下:
1 <!DOCTYPE html> 2 <html lang="en"> 3 4 <head> 5 <meta http-equiv="content-type" content="text/html; charset=utf-8" /> 6 <title>Test</title> 7 </head> 8 9 <body> 10 <script type="text/javascript"> 11 document.write("<p>This is inserted.</p>"); 12 </script> 13 </body> 14 15 </html>
上面第11行Javascript代码在body元素内部直接插入一个p元素和相关文本:“<p>This is inserted.</p>”,效果如下:
这种方式的缺点很明显,Javascript代码和HTML代码掺杂在一起,无法分离。因为document.write( )是在当前位置插入,需要在哪里插入就得把这句Javascript代码嵌入到哪里。有人可能会想我难道不能用获取body元素之后,给body元素插入一个文本子节点(字符串"<p>This is inserted.</p>")的办法来实现吗?
实际测试可以发现这么做行不通,最终得到的效果是下面这样的:
另外一种方法是innerHTML,基本上所有浏览器都支持。innerHTML比较粗犷,直接访问某个元素的innerHTML属性来进行读写操作:element.innerHTML="<p> this is <em> emphasize</em> text</p>"
DOM方法
DOM方法改变的是浏览器的DOM树,所以使用浏览器查看网页源码,并不能看到HTML源码有所变化。
createElement( )方法创建一个元素节点,这个新创建出来的元素节点是个游离在浏览器DOM树之外的孤儿节点,术语称为“文档碎片(document fragment)”,它与浏览器DOM树还没有产生关联,不会得到显示,但是它已经是一个元素节点了,具有DOM属性,支持相关DOM操作。
appendChild( )方法将一个节点追加到DOM树上。
createTextNode( )方法是创建一个文本节点。
insertBefore( )方法是插入一个元素节点到指定元素之前。这个方法有些不合直觉,它需要parent父元素、待插入的元素new、插入到哪个元素之前target:parent.insertbefore(new, target)。父元素可以直接通过target.parentNode拿到,也就是可以写成target.parentNode.insertbefore(new, target)。(插入一个元素实际并不需要父元素,这里说明制定标准的那群人完全脱离现实,纯粹在纸上谈兵,没注意到实际写代码时知道一个元素的确切位置之后就可以进行插入了。)
没有insertAfter( )方法,可以通过判断target元素是否是最后一个元素来变相实现,如果是最后一个元素,插入到target后面其实就是追加到现有元素节点之后,也就可以用appendChild( )方法来实现。如果不是最后一个节点,可以通过target.NextSibling属性获取到下一个节点,然后在这个节点前面插入。
Ajax技术
ajax是一种技术,不是具体的某个对象或者DOM方法、属性。ajax是一种“在不重新加载整个网页的情况下,能够部分更新网页”的技术。举个例子,以前上网看帖子,都是一页一页的翻,第1页、第2页、第9页等等,就像下面这样:
现在一种新的翻页方法它不再显示有多少多少页,它在最底部写上“拉取加载下一页”,比如朋友圈加载,截图像下面这样:
上面这种就有点像不重新加载整个网页的情况下,部分更新了网页,上面截图里面它没有像第一次打开朋友圈那样去重新加载整个朋友圈页面,它只是发出一个请求,继续加载动态,一直往下填充,现有已经加载的页面框架都不需要重新请求。这样就显著减小了传输的数据大小,压缩了开销,提高了用户体验。但是也会有缺点,有些设计不好的页面,有时候想要后退发现没法后退。
Ajax技术是通过Javascript实现的,这是一种很好的反爬虫手段,因为很多爬虫都不能解析Javascript代码。Ajax技术的核心是XMLHttpRequest对象,XMLHttpRequest是一个Javascript内置对象(还记得第3章说过另外两种对象是用户自定义对象和浏览器提供的宿主对象)。
XMLHttpRequest对象强大在它能进行网络请求,这就为Javascript编程提供了很强有力的支持,试想一个不支持网络编程或者网络访问的编程语言会有什么大作用呢?现如今不通过网络交互的程序还有多少呢?
代码实践错误解决笔记
在实际编写7.4节的代码时,发现跟着书本不能完全复现。首先检查代码没有问题,但是浏览器里面就是没有任何内容显示,完全不符合预期。打开chrome浏览器的控制台发现有报错,截图如下:
切换到Console页面,发现说是网络错误,没法获取到文件。检查文件路径确认是正确的,然后看上面报错:“Access to XMLHttpRequest at xxx from origin xxx has been blocked by CORS policy”。看到关键词CORS,大概就知道了这是浏览器的跨域限制,书本后面也说chrome浏览器从本地硬盘加载文件时会出现这种错误,实际测试IE浏览器、Safari浏览器都会出现同样的错误。
为了解决这个问题,开始我上网搜了一下,网上有说在Windows系统里面给chrome浏览器快捷方式添加允许跨域访问的参数就能解决,实际测试没有效果。
最简单的解决办法就是自己搭建一个后台服务,然后把代码和文件都交给同一个后台,这样就避免了跨域问题。不过搭建一个后台服务,比如nginx比较费时费力,因为还要得会配置它,不熟悉的人可能就卡死在这里了。懂的人也得浪费时间下载、安装、配置、运行。
其实我们只需要一个最简单的后台服务,只要它能够返回HTML页面,并且对 XMLHttpRequest 对象的 open( ) 函数请求的URL能够响应返回文本即可。因此我用golang语言写了一个非常简单的后台服务,双击即可启动使用。
这里提供一个Windows 64位系统下面编译好可以用的简便后台:点此下载。(该程序源码在文章最下面给出,熟悉golang的可以根据需要自行修改配合测试)
下载好ajax.exe后台服务程序,把它和test.html文件(你的源码)放在一个目录下面,双击启动 ajax.exe 程序。
不要关闭 ajax.exe 程序(放在那里不管),然后打开浏览器,输入127.0.0.1回车,就能看到下面的页面了,这里面的 test.html就是自己根据书本写的测试代码了,点击它进行访问。
需要说明的是,open( )函数请求的URL需要替换为“http://127.0.0.1/example”,因为 XMLHttpRequest 对象的 open( ) 函数只有请求http://127.0.0.1/example这个URL的时候,才会返回数据。
如果你的ajax.exe不能启动起来,很可能是这个程序用到的端口和别的程序冲突了,在命令行中进入到ajax.exe的目录下,输入 ajax.exe -h 回车,用 -h 这个选项可以查看到 ajax.exe 这个程序支持更改默认使用的80端口,还支持 -t 选项指定http://127.0.0.1/example这个URL返回的数据。
如果你使用 ajax.exe -p 34563 这种形式改变了默认端口,那么你的XMLHttpRequest 对象的 open( ) 函数请求的URL也要改变,变成 http://127.0.0.1:34563/example 这个URL,在浏览器中输入的127.0.0.1也要改成127.0.0.1:34563
指定自定义的返回数据截图如下:
后台服务go源码
package main import ( "flag" "fmt" "net/http" ) var ( port = "80" text = "" ) func example(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, text) } func main() { flag.StringVar(&port, "p", "80", "specify listen port, use 80 if not specify") flag.StringVar(&text, "t", "this was loaded asynchronously.", "response text") flag.Parse() fmt.Printf("port:%s, text:%s ", port, text) http.Handle("/", http.FileServer(http.Dir("./"))) http.HandleFunc("/example", example) fmt.Println(http.ListenAndServe("0.0.0.0:"+port, nil)) }
可根据需要自行修改编译。