第6章 HTML5页面加载

6.1 HTML页面加载过程


HTML文档的加载除了要获取HTML文档本身,可能还要获取额外的资源,例如图片视频等多媒体资源。浏览器将会按照一定流程获取和渲染这些资源,并最终完成页面加载过程。

  • 6.1.1 页面加载过程概述
  • 6.1.2 请求资源
  • 6.1.3 加载过程耗时分析

6.1.1 页面加载过程概述


浏览器解析HTML过程中,会根据标签创建DOM树上的元素,解析CSS属性,计算元素大小和位置,执行JavaScript代码,同时根据解析得到的内容渲染页面,并转换为可视/听的内容展现给用户。这是浏览器渲染页面的主线。

通常为了减少用户的等待,在页面还未全部解析的情况下,浏览器会把已经解析得到的元素先渲染出来并展示。但此时由于JavaScript和CSS等资源可能还没有来得及处理,先展示出的内容可能和最终展示的部分并不相同,比如缺乏格式。

6.1.2 请求资源


HTML文档还可以引用多种外部资源,包括多媒体资源,也包括JavaScript和CSS这样的代码资源。这些资源的获取可以与HTML文档解析渲染同时进行。如下面代码,引用了两张图片和一个代码文件。


<img src='1.jpg'>
<script src='a.js'>
<img src='2.jpg'>
					

开启浏览器开发者工具后,打开或刷新该页面,在网络选项卡中可看到浏览器请求的资源详情如下图所示。

6.1.3 加载过程耗时分析


在开发者工具性能标签中,点击录制按钮后,浏览器将记录页面加载过程各部分耗时情况。前面提到的示例页面的加载过程耗时结果如下图所示。

6.2 浏览器缓存


浏览器加载耗时大致可以分成资源下载和渲染计算两类。资源下载通常需要通过网络,完成的时间也取决于网络连接的速度。浏览器可以把不太可能改变的静态资源缓存下来,再次访问相同网页,或者多个页面引用了相同资源时,可以避免再通过网络下载,而直接可以从缓存读取,从而提高页面加载速度。

  • 6.2.1 缓存的作用
  • 6.2.2 避免缓存的副作用

6.2.1 缓存的作用


浏览器在打开网站时,下载的资源可能会被写入用户计算机的本地存储(比如硬盘),这里面有一些资源是不太可能改变的,比如一幅图片。下次再访问这个网页,或者访问其它引用这个资源的网页时,浏览器如发现本地存储中存在这个资源,则会直接从本地存储读取,而避免通过网络再次下载。

安装python3后,通过命令python -m http.server在上一节的res_load.html页面路径下启动一个HTTP服务器。然后通过浏览器访问页面http://127.0.0.1:8000/res_load.html。第一次加载时,网络选项卡中内容如下图所示。

两张图片传输的数据大小分别是1.3MB和951kB,JavaScript文件a.js传输大小是210B,与这些文件的实际大小大致符合(网络传输过程中可能会对资源进行压缩)。但这时立即刷新该页面,看到网络标签卡中的内容如下图所示。

6.2.2 避免缓存的副作用


在使用浏览器时,如果怀疑遇到缓存导致的问题时,可以通过浏览器开发者工具关闭浏览器缓存,或者选择开启开发者工具时禁用缓存。也可以选择清除缓存。

从HTML5开发者的角度,要避免缓存带来问题,首先要了解浏览器缓存机制。对于易被缓存的资源,不要直接编辑内容。比如在HTTP服务器上直接替换图片,但不修改图片名称或路径。

实践中的做法是,对于易被缓存资源的命名,加入这个资源修改的时间或者加入资源文件的哈希值,这样文件被修改,名称也会变化,同时应该更新html文档中对这些资源的引用。另外也可以不重命名资源,而只修改html文件。比如下面代码给script标签的src中url增加一个没有实际用途的参数,每次更新文件后修改这个参数。


						<script src='a.js?version=1'></script>
					

但这种方法可能会影响某些浏览器的缓存策略,从而导致缓存无法工作。

6.3 动态加载


当HTML5应用中包含大量资源,但用户使用时往往不会立刻看到或用到这所有的资源。动态加载指根据一定的条件触发资源加载,懒惰加载则特指仅当用户需要时才加载相应资源。

  • 6.3.1 Ajax技术
  • 6.3.2 根据滚动条的位置触发动态加载
  • 6.3.3 根据时间触发动态加载

6.3.1 Ajax技术


Ajax是Asynchronous JavaScript and XML(异步的 JavaScript 和 XML)的缩写,是JavaScript中实现HTML应用与HTTP服务器动态通信的方法。

Ajax可以在页面不刷新的情况下向服务器发送请求,既可以向服务器传递信息,又可以从服务器下载新的内容。

6.3.2 使用fetch和服务器通信


fetch是JavaScript中一个新的用于和服务器通信的接口,和XMLHttpRequest类似,但使用起来更简洁。fetch使用了JavaScript的异步特性,是基于Promise实现的,关于Promise的详细内容见7.4.5小节。下面给出了使用fetch获取资源“a.txt”的示例代码。


<meta charset='utf8'>
<button onclick="get_data()">点击发送Ajax请求
<script>
    function get_data() {
        fetch("a.txt").then((response)=>{
            return response.text();
        }).then((data)=>{
            alert("a.txt内容是:" + data);
        })
    }
</script>
				    

6.3.3 根据时间触发动态加载


除了根据用户动作实现动态加载外,还可以根据时间实现定时加载。常用的场景有自动轮播或者榜单的定时刷新等。

setInterval(func, delay, [可选参数列表])函数可以设置一个函数func和一个以毫秒为单位的时间间隔delay,每隔delay毫秒,函数func就会被调用一次。可以用于周期性加载资源

setTimeout(func, delay, [可选参数列表])和setInterval类似,但setTimeout只会让函数执行一次。

6.4 使用JavaScript监控页面加载和运行情况


页面加载时间长短,页面加载过程中出现的错误等问题都关系到用户体验。HTML5应用需要在用户的浏览器中执行,由于每个用户网络环境、设备型号、软件版本等条件差异,可能遇到多种多样的问题。

JavaScript可以通过监听特定事件和访问浏览器接口,监控页面加载和运行过程中的问题,帮助开发者和应用管理者了解HTML5程序在用户浏览器中的实际表现。

  • 6.4.1 加载过程中可能遇到的问题
  • 6.4.2 加载过程中触发的事件
  • 6.4.3 获取页面加载时间
  • 6.4.4 捕获运行异常

6.4.1 页面加载过程中可能遇到的问题


最常见的是页面加载时间问题。页面加载时间长是导致用户体验差的重要原因。页面加载时间和用户的网络环境、HTTP服务器的负载、用户设备的性能、用户浏览器、操作系统版本等多种因素相关

因为页面加载时间受到很多因素影响,且部分因素来自用户方面,难以在测试环境中准确衡量。故对重要页面有必要在浏览器中测量页面加载时间,并同时收集可能影响页面加载的信息。

6.4.2 加载过程中触发的事件


loadstart是资源开始加载时触发的事件。

window的load时间会在整个页面加载完毕后被触发,包括CSS、JavaScript和图片等。通常可以选择使用这个事件触发的事件作为页面加载完成的时间。

DOMContentLoaded是HTML文档加载和解析完成时触发的,但这个时候图片、代码等资源可能还没有下载完毕和加载好。下面代码展示了在document上绑定DOMContentLoaded事件。


document.addEventListener("DOMContentLoaded", (event) => {
	console.log("DOM 加载完毕");
});
					

另外,还可以通过document.readyState判断HTML文档是否已经加载和解析完成。文档加载解析完成后这个属性变为“complete”。

6.4.3 获取页面加载时间


window.performance.timing中记录了页面加载相关的时间。在它的属性中,navigationStart是浏览器开始要加载当前页面的时间。domainLookupStart和domainLookupEnd是DNS开始和结束的时间。connectStart是浏览器和HTTP服务器开始建立连接的时间。connectEnd是建立连接完成的时间。domComplete是HTML文档解析和加载完成的时间。loadEventStart和loadEventEnd是load时间开始和结束的时间。

可以通过window.performance.timing中的属性计算得到页面加载时间,但可能有浏览器不支持该接口,而且该接口也不在最新的标准中。当浏览器不支持该方法时,可以在上一小节介绍的load事件或DOMContentLoaded事件中得到相应加载阶段完成的时间。

对于页面加载开始的时间,可以在HTML文件最开始添加一段代码,使用Date.now获取近似的页面加载开始时间

6.4.4 捕获运行异常


浏览器中,资源加载和JavaScript代码运行过程中都可能遇到异常。比如指定资源不存在,或者服务器返回错误状态码,以及JavaScript代码中本身的错误。

JavaScript可以使用try-catch代码块捕捉特定范围内的代码运行异常,还可以通过error事件捕获更大范围内的异常。对于图片等静态资源,可以在标签添加onerror属性绑定函数处理其加载失败事件。

6.5 小结


本章介绍了HTML5页面加载相关的内容。广义上说,页面加载包括从浏览器发出请求,服务器返回请求,浏览器收到响应后解析HTML文档,构建DOM,并再请求资源文件(如果需要),最终展现给用户。

狭义的页面加载则主要限于浏览器解析和处理HTML文档及其依赖资源的部分。这一部分是实际影响用户体验的关键,但脱离了HTTP服务器的控制。只有了解HTML加载机制,才能更好的开发HTML5应用程序和设计HTML5程序性能及功能监测机制。

6.6 课堂练习—动态加载的HTML5相册


第二章课堂练习实现了HTML5相册,但相册应用打开时就会请求并载入所有图片。当相册中图片数量很多时,页面加载速度会变慢,页面消耗资源也很大。本节将在其基础上实现动态加载功能。

  • 6.6.1 使用JavaScript生成img标签
  • 6.6.2 点击加载图片

6.6.1 使用JavaScript生成img标签


要实现动态加载,首先要实现JavaScript动态产生img标签,而不是直接在HTML代码中写出所有图片的img标签。

第二章中相册分为风景、动物两个大类,风景中又分成秋季和夏季两个小类。可以按照类别定义图片列表。代码的HTML部分将变得非常简单,仅有一个空的div标签用于存放动态加载的内容。


<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>我的相册动态版
</head>
<body>
    <div id='content'> 
    </div>
</body>
					

JavaScript代码部分定义了相册分类解构,以及每个分类包含的图片资源,并实现了根据这个结构生成HTML代码的方法。


<script>
	// 定义相册的结构
	let album = {
		'album': { // album 中包含的时相册的子类
			'风景': {
				'album': { // 风景子类包含另外两个子类
					'秋季': {
						// imgs 中包含这个子类的照片列表
						'imgs': ['1.jpg', '2.jpg', '3.jpg', '4.jpg', '5.jpg', '6.jpg', '7.jpg']
					},
					'夏季': {
						'imgs': ['8.jpg']
					},
				}
			},
			'动物': { 
				'imgs': ['9.jpg', '10.jpg']
			}
		}
	}
	// 下面是生成相册HTML标签的方法
	function loadAlbum(title, album, i) {
		let html = ''; // 先定义空的字符串
		if (title) {   // 如果有标题则生成标题
			html += '<h' + i + '>' + title + '</h'+i+'>
\n'; i ++; } if (album['imgs']) { // for (let img of album['imgs']) { html += '<img width="45%" src="' + img + '">\n'; } } if (album['album']) { for (let sub_album in album['album']) { html += loadAlbum(sub_album, album['album'][sub_album], i); } } return html; } // 下面语句调用 loadAlbum 函数,并把它生成的HTML代码放到id为content的标签内 content.innerHTML = loadAlbum(null, album, 1); </script>

这里通过loadAlbum生成的HTML代码几乎和第二章相册的HTML5代码一致。这里为了让页面长度更长把图片宽度从18%增加到45%,这样每行只显示两张图片。

6.6.2 点击加载图片


这一小节实现打开页面仅显示相册分类标题,当点击加载图片按钮时再加载一个分类的照片。给loadAlbum添加一个参数loadAlbum,当loadAlbum为true时和第一小节相同,直接显示出所有照片;为false时,不显示照片,而显示一个按钮。修改后的loadAlbum代码如下所示。


function loadAlbum(title, album, i, loadImage) {
	let html = '';
	if (title) {
		html += '<h' + i + '>' + title + '</h'+i+'>
\n'; i ++; } if (album['imgs']) { if (loadImage) { for (let img of album['imgs']) { html += '<img width="45%" src="' + img + '">\n'; } } else { html += '
'; } } if (album['album']) { for (let sub_album in album['album']) { html += loadAlbum(sub_album, album['album'][sub_album], i, loadImage); } } return html; }

6.6.2 点击加载图片


在loadImage为false时,函数loadAlbum不生成img标签,而把图片列表通过JSON.stringify方法转换为json格式,作为loadImage函数的参数,放到按钮的onclick属性中。点击按钮后会使用这个json格式的图片列表作为参数调用loadImage函数如下。


function loadAlbum(title, album, i, loadImage) {
	let html = '';
	if (title) {
		html += '<h' + i + '>' + title + '</h'+i+'>
\n'; i ++; } if (album['imgs']) { if (loadImage) { for (let img of album['imgs']) { html += '<img width="45%" src="' + img + '">\n'; } } else { html += '
'; } } if (album['album']) { for (let sub_album in album['album']) { html += loadAlbum(sub_album, album['album'][sub_album], i, loadImage); } } return html; }

function loadImage(currentElement, imageList) {
	let html = '';
	for (let img of imageList) {
		html += '<img width="45%" src="' + img + '">\n';
	}
	currentElement.parentElement.innerHTML = html;
}
					

这个函数第一个参数是按钮元素本身,在按钮onclick属性中使用关键字this代表按钮元素本身,通过按钮元素的parentElement属性得到按钮元素的父元素,即外层的div元素,通过其innerHTML属性修改其HTML内容,把图片显示出来。页面刚打开时,内容如图所示。

点击按钮后按钮消失,这个分类下的所有图片都显示在这个分类标题下方。