【简介】Ajax 是高性能 JavaScript 的基础。它可以通过延迟下载体积较大的资源文件来使得页面加载速度更快。它通过异步的方式在客户端和服务端之间传输数据,从而避免了页面资源一窝蜂地下载。它甚至可以只用一个 HTTP 请求就获取整个完整的页面资源。选择适合的传输方式和最有效的数据格式,可以显著改善用户和网站的交互体验。
1. 请求数据(Request Data)
Ajax 是一种与服务器通信而不需重载页面的方法;数据可以从服务器获取或发送给服务器。有多种不同的方法建立这种通信通道,每种方法都有各自的优点和限制。
常用的向服务器请求数据的技术有五种:
- XMLHttpRequest (XHR)
- Dynamic script tag insertion 动态脚本注入
- iframes
- Comet
- Multipart XHR
在现代高性能 JavaScript 中使用的三种技术是:XHR、动态脚本注入和 multipart XHR。
1-1. XMLHttpRequest
XMLHttpRequest(XHR)是目前最常用的技术,它允许异步发送和接收数据。所有的主流浏览器对它都提供了完善的支持,而且他还能精确地控制发送请求和数据接收。你可以在请求中添加任何头信息和参数(包括 GET 和 POST),并读取服务器返回的所有头信息,以及响应文本。
<script>
var url = "/data.php";
var params = ['id=342', 'limit=20'];
// 实例化一个 XHR 对象
var req = new XMLHttpRequest();
req.onreadystatechange = function() {
// readyState等于4表示整个响应已接收完毕
if (req.readyState == 4) {
// 获取响应头信息
var responseHeaders = req.getAllResponseHeaders();
// 获取数据
var data = req.responseText;
// 数据处理
// code...
}
}
// 封装成 /data.php?id=342&limit=20
req.open('GET', url+'?'+params.join('&'), true);
// 设置请求头信息
req.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
// 发送一个请求
req.send(null);
</script>
通过监听 readyState 值等于 4 表示整个响应已接收完毕,可进行操作。而通过监听 readyState 值等于 3,可以与正在传输中的服务器响应进行交互,此时响应信息还在传输过程中。
req.onreadystatechange = function() {
if (req.readyState == 3) {
// 未接收完全的响应信息
var dataSoFar = req.responseText;
// 与未接收完成的响应信息做交互
// code...
} else if (req.readyState == 4) {
// 已接收完全的响应信息
var data = req.responseText;
// 处理响应信息
// code...
}
}
使用 XHR 时,POST 和 GET 的对比。首选应该是 GET,经过 GET 请求的数据会被缓存起来,如果需要多次请求同一数据的话,它会有助于提升性能。只有当请求的 URL 加上参数的长度超过 2048 个字符时,才应该用 POST 获取数据。
1-2. 动态脚本注入
这种技术克服了 XHR 的最大限制:它能跨域请求数据(XHR 受浏览器同源策略影响,不能做跨域请求数据)。
<script type="text/javascript">
// 创建一个<script>标签
var scriptElement = document.createElement('script');
// 设置<script>标签的src路径
scriptElement.src = 'http://other-domain.com/javascript/lib.js';
// 将<script>标签插入到<head>标签中执行,即可获取lib.js
document.getElementsByTagName('head')[0].appendChild(scriptElement);
</script>
这种方式虽然能够进行跨域请求,但是提供的控制十分有限——不能设置请求的头信息;参数的传递也只能使用 GET 方式,而不能是 POST 方式;不能设置请求的超时处理或重试;不能访问请求的头信息;不能把整个响应作为字符串来处理。
关于最后一点「不能把整个响应作为字符串来处理」,因为响应消息作为脚本标签的源码(服务器传回来的资源 lib.js,都是 JavaScript 代码),而不能是纯 XML、纯 JSON 或其他任何格式的数据,无论哪种格式都必须封装在一个回调函数中。
function jsonCallback(jsonString) {
var data = eval('(' + jsonString + ')');
// 处理数据...
}
而且 lib.js 中需要把数据封装在 jsonCallback 函数中:
jsonCallback({"status":1, "colors":["#fff", "#000", "ff0000"]});
虽然有很多限制,但这项技术的速度非常快,响应消息是作为 JavaScript 执行,而不是作为字符串需要进一步处理。正因为如此,它有潜力成为客户端获取并解析数据最快的方法。
在使用这项技术的时候,有一点需要非常小心。JavaScript 没有任何权限和访问控制的概念,因此使用动态脚本注入添加到页面中的任何代码都可以完全控制整个页面。包括修改任何内容,把用户重定向到其他网站,甚至跟踪用户在页面上的操作并发送数据到第三方服务器。
1-3. Multipart XHR
multipart XHR (MXHR)允许客户端只用一个 HTTP 请求就可以从服务端想客户端发送多个资源。它通过在服务端将资源(CSS 文件、HTML 片段、JavaScript 代码或 base64 编码的图片)打包成一个由双方约定的字符串分割的长字符串并发送到客户端。然后用 JavaScript 代码处理这个长字符串,并根据它的 mime-type 类型和传入的其他「头信息」解析出每个资源。
我们来看一个用来获取多张图片的请求发送到服务器的过程。
首先,客户端向服务端 rolluo_images.php 请求数据:
req.open('GET', 'rollup_images.php', true);
req.onreadystatechange = function() {
if (req.readyState ==4) {
splitImages(req.responseText);
}
};
req.send(null);
服务器接收到请求,读取三张图片并将它们转换成字符串,返回给客户端:
<?php
// 读取图片并将它们转换成 base64 编码的字符串
$images = array('a.jpg', 'b.jpg', 'c.jpg');
foreach($images : $image) {
$image_fh = fopen($image, 'r');
$image_data = fread($image_fh, filesize($image));
$payloads[] = base64_encode($image_data);
fclose($image_fh);
}
// 把字符串合并成一个长字符串,然后输出它
// chr(1) 表示「\u0001」 该字符不会出现在任何 base64 字符串中
$newline = chr(1);
echo implode($newline, $payloads);
?>
客户端接收到数据后,由 splitImages 函数处理,该函数将连接后的字符串通过 split 函数分解成三段存入数组中,然后对数据循环,每次都创建一个 img 元素,并将这个元素插入到页面中。处理图片时不需要再将 base64 字符串转换成二进制,使用 data:URL 的方式即可创建,并指定 mime-type 为 image/jpeg。
通过这样的方式,就可以在一次 HTTP 请求中,向浏览器传送三张图片。除了图片之外,这种方式也可以扩展到其他资源类型(JavaScript 文件、CSS 文件、HTML片段以及多种类型的图片都能合并成一次响应)。下面函数用于将 JavaScript 代码、CSS 样式和图片转换成浏览器可用的资源:
<script type="text/javascript">
// 处理图片
function handleImageData(data, mimeType) {
var img = document.createElement('img');
img.src = 'data:'+mimeType+';base64,'+data;
return img;
}
// 处理CSS样式
function handleCss(data) {
var style = document.createElement('style');
style.type = "text/css";
var node = document.createTextNode(data);
style.appendChild(node);
document.getElementsByTagName('head')[0].appendChild(style);
}
// 处理JavaScript
function handleJavaScript(data) {
eval(data);
}
</script>
随着请求的资源量越来越大,因此有必要在每个资源收到时就立刻处理,而不是等到整个响应消息接收完成。因此,处理 MXHR ,我们需要监听 readyState 为 3 的状态。
<script type="text/javascript">
var req = new XMLHttpRequest();
var getLatestPacketInterval, lastLength = 0;
req.open('GET', 'rollup_images.php', true);
req.onreadystatechange = readyStateHandler;
req.send(null);
function readyStateHandler() {
if (req.readyState == 3 && getLatestPacketInterval == null) {
// 开始轮询
getLatestPacketInterval = window.setInterval(function () {
getLatestPacket();
}, 15);
}
if (req.readyState == 4) {
// 停止轮询
clearInterval(getLatestPacketInterval);
// 获取最后一个数据包
getLatestPacket();
}
}
// 获取并处理数据包
function getLatestPacket() {
var length = req.responseText.length;
var packet = req.responseText.substring(lastLength, length);
// 处理数据包函数,函数内容没有写
processPacket(packet);
// lastLength是一个全局变量,用于记录当前数据包的总长度
lastLength = length;
}
</script>
当 readyState 为 3 的状态,第一次触发,启动一个定时器,每隔 15 毫秒检查一次响应中的新数据。数据片段会被收集起来(lastLength 作为一个全局变量,不随函数的结束而消失,起到收集数据的作用),直到发现一个分隔符,然后就把遇到分隔符之前收集的所有数据作为一个完整的资源进行处理。
使用这种技术有一些缺点,其中最大的缺点是不能被浏览器缓存。因此,这种技术在一些特定的情况下使用:
- 页面包含了大量其他地方用不到的资源(因此也无须缓存),其他事图片。
- 网站已经在每个页面中使用一个独立打包的 JavaScript 或 CSS 文件以减少 HTTP 请求,因为对每个页面来说都是唯一的,所以不需要从缓存中读取数据,除非重载页面。
由于 HTTP 请求时 Ajax 中最大的瓶颈之一,一年才减少其需要的数量会对整个页面性能有很大的影响。尤其是当你将 100 个图片请求转化为一个 MXHR 请求时。
附: 欢迎大家关注我的新浪微博 - 一点编程,了解最新动态 。