JavaScript性能优化:(七)Ajax

AJAX stands for Asynchronous JavaScript and XML. In a nutshell, it is the use of the XMLHttpRequest object to communicate with server-side scripts. It can send as well as receive information in a variety of formats, including JSON, XML, HTML, and even text files. AJAX’s most appealing characteristic, however, is its “asynchronous” nature, which means it can do all of this without having to refresh the page. This lets you update portions of a page based upon user events.

请求数据

XMLHttpRequest

常见的使用 XMLHttpRequest 技术请求数据的方式如下:

1
2
3
4
5
6
7
8
9
10
11
let req = new XMLHttpRequest();
req.onload = function() {
};
req.onerror = function() {
};
req.open('GET', url, true);
req.send();

尽管 XHR 提供了高级的控制,但还是有一些缺点:无法使用 XHR 技术从不同源获取数据。

动态脚本注入

这种技术克服了 XHR 的最大限制:能跨域请求数据。本质上,这种技术是个 Hack。

1
2
3
let scriptElement = document.createElement('script');
scriptElement = 'http://';
document.head.appendChild(scriptElement);

但是与 XHR 相比,动态脚本注入提供的控制是有限的:

  • 不能设置请求的头部信息
  • 参数传递只能使用 GET 方式,而不是 POST 方式
  • 不能设置请求的超时处理或重试
  • 失败了也不一定知道
  • 必须等待所有数据都已返回,才可以访问它们
  • 不能访问请求的头信息,也不能把整个响应消息作为字符串来处理

特别重要的一点是,响应消息作为脚本标签的源代码,它必须是可执行的 JavaScript 代码。我们不能使用纯 XML、纯 JSON 或其他任何格式的数据,无论哪种格式,都必须封装在一个回调函数中。

尽管限制很多,但是这项技术的速度却非常快。

使用这种技术时,必须注意我们所请求的 JavaScript 代码内容是否完全在自己控制之中。JavaScript 没有任何权限和访问控制的概念,使用动态脚本注入技术添加到页面中的任何代码都可以完全控制整个页面,既是是引自外部的代码也不例外。

Multipart XHR

MXHR 允许客户端只用一个 HTTP 请求就可以从服务器端向客户端传送多个资源。它通过在服务器端将资源(CSS 文件、HTML 片段、JavaScript 代码或 base64 编码的图片)打包成一个由双方约定的字符串分割的长字符串送到客户端。然后使用 JavaScript 处理这个长字符串,并根据它的 mime-type 类型和传入的其他“头信息”解析出每个资源。

例如,客户端请求图片资源,服务器使用 base64 将图片编码为字符串然后传递给客户端,当客户端收到后可以使用如下 JavaScript 代码进行处理:

1
2
3
4
5
6
7
8
9
10
11
12
function splitImages(imageString) {
// 假设服务器端编码的图片之间只用一个简单的 Unicode 编码的字符 1 连接
let imageData = imageString.split('\u0001');
let imageElement = document.createElement('img');
let imageNode = null;
for (let i = 0, len = imageData.length; i < len; i++) {
imageNode = imageElement.cloneNode(true);
imageNode.src = `data:image/jpeg;base64,${imageData[i]}`;
document.body.appendChild(imageNode);
}
}

以上代码中函数将连接后的字符串按一定格式分解为几段。每一段用来创建一个图片元素,然后将图片元素插入到页面中。图片不是由 base64 字符串转换为二进制,而是使用 data:URL 的方式创建,并指定 mime-typesimages/jpeg

这种方式也可以扩展到其他资源类型。JavaScript 文件、CSS 文件、HTML 片段以及多种类型的图片都能合并成一次响应。任何数据类型都可以被 JavaScript 作为字符串发送。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function handleImageData(data, mimeType) {
const img = document.createElement('img');
img.src = `data:${mimeType};base64${data}`;
return img;
}
function handleCSS(data) {
const style = document.createElement('style');
const node = document.createTextNode(data);
style.appendChild(node);
document.head.appendChild(style);
}
function handleJavaScript(data) { // 不建议
eval(data);
}

由于 MXHR 响应消息的体积越来越大,因此我们有必要在每个资源收到时就立刻处理,而不是等到整个响应消息全部接收完成再进行处理。这可以通过监听 readyState === 3 的状态,使用定时器按一定时间间隔检查响应来实现。

MXHR 技术有个最大的缺点,以这种方式获得的资源不能被浏览器缓存。

发送数据

XMLHttpRequest

XHR 技术同样可用于把数据传回服务器·

Beacons(信标)

这项技术非常类似动态脚本注入。使用 JavaScript 创建一个新的 Image 对象,并把 src 属性设置为服务器上脚本的 URL。该 URL 包含了我们要通过 GET 传回的键值对数据。注意,我们并不会创建 img 元素或把它插入 DOM 树中。

1
2
3
4
5
6
7
const url = '/status_tracker.php';
const params = [
'step=2',
'time=134152526190'
];
(new Image()).src = `${url}?${params.join('&')}`;

服务器会接收数据并保存下来,它无须向客户端发送任何回馈信息,因此没有图片会实际显示出来。这是给服务器回传信息最有效的方式。它的性能开销开销很小,而且服务器的错误完全不会影响到客户端。

图片信标很简单,但也意味着它能做的事情是有限的。我们无法发送 POST 数据,而使用 GET 方法 URL 的长度有最大上限,所以我们可以发送的数据长度被限制得相当小。

我们可以接收服务器返回的数据,但仅限于非常少得几种方式:

  • 监听 Image 对象的 load 事件
  • 检查返回图片的宽高,利用这些数字通知服务器的状态

信标是向服务器回传数据最快且最有效的方式,服务器根本不需要发送任何响应正文,因此我们也无需担心客户端下载数据。唯一的缺点就是能接收到的响应内心是有限的。

数据格式

常见的用于数据传输的数据格式有 XML、XPath、JSON、JSONP、HTML 和自定义格式等,通常来说数据格式越轻量级越好,JSON 和字符串分隔的自定义格式是最好的。如果数据集很大并对解析时间有要求,那么将从以下两种格式中做出选择:

  • JSONP数据:使用动态脚本注入获取。它把数据当作可执行 JavaScript 而不是字符串,解析速度极快。能够跨域使用,但涉及敏感数据时不应使用它
  • 字符分隔的自定义数据格式:使用 XHR 或者动态脚本注入获取,使用 split() 解析。这项技术解析大数据集比 JSONP 略快,而且通常文件尺寸更小

缓存数据

最快的 Ajax 请求就是没有请求。有两种主要方法可避免发送不必要的请求:

  • 服务端,设置 HTTP 头信息以确保响应数据被浏览器缓存
  • 客户端,把获取到的信息存储到本地,从而避免再次请求

第一种技术使用最简单而且好维护,而第二种则给予开发人员最大的控制权。

设置 HTTP 头信息

如果希望 Ajax 响应能够被浏览器缓存,那么必须使用 GET 方式发出请求。但这还不够,还必须在响应中发送正确的 HTTP 头信息。Expires 头信息会告诉浏览器应该缓存响应多久。它的值是一个日期,过期之后,对该 URL 的任何请求都不再从缓存中获取,而是会重新访问服务器。一个典型的 Expires 头信息如下:

Expires: Mon, 28 Jul 2014 23:30:00 GMT

这种特殊的 Expires 头信息告诉浏览器缓存此响应到 2014 年 7 月。

本地数据存储

直接把从服务器接收到的数据储存起来。我们可以把响应文本保存到一个对象中,以 URL 为键值作为索引。

总的来说,设置一个 Expires 头信息是更好的方案。它实现起来比较容易,而且其缓存内容能够跨页面和跨会话。