这一章的作者是前面写了字符串和正则表达式部分的steven levithan,高性能Ajax需要根据项目的具体情况和要求,选择合适的数据格式和数据传送技术。
- 纯文本和HTML只适用于特定的场合,但是可以客户端的CPU消耗;
- XML被广泛应用和支持,但是比较笨重且解析缓慢;
- JSON是轻量级的,解析速度快(是js原生对象而不是字符串),通用性与XML相当;
- 字符分隔的自定义格式更加轻量级,解析大量数据时非常快,但是需要在服务器和客户端都进行特殊处理。
- XHR在同域名下接收数据时具有很好的灵活性和可控性,不过解析速度会可能会收到影响,因为接收到的数据是需要处理的字符串;
- 动态脚本注入可以跨域接收数据,并且数据是不需要解析的原生javascript或json代码,但是可控性不强,不能读取头信息和响应代码;
- Multipart XHR接收数据时可以用来减少请求数,她能在同一个响应中处理多个文件和多种文件类型,但是不能缓存接收到的数据。
- 图片信标是一种简单而有效的发送数据方法,当然XHR也可以用POST方法发送大量数据。
加速Ajax
- 减少请求,通过合并javascript和css文件,或者使用Multipart XHR;
- 缩短页面加载时间,主要内容先加载,之后用ajax来获取次要的内容;
- 在服务器端做好容错,确保代码错误不会输出给用户;
- 根据实情,选择何时使用成熟的ajax库,何时自己编写底层的ajax代码。
请求数据
有五种常用的技术,用于向服务器请求数据,其中前三种比较常见,后两种(iframe/comet)一般在极端的情况下使用:
- XMLHttpRequest (XHR)
- Dynamic script tag insertion (动态脚本注入)
- Multipart XHR(基本原理同xhr)
- iframes
- Comet (基于HTTP长连接的“服务器推”技术,服务器端推送,事件驱动,类nodejs,一直把这个和java的tomcat搞混了:()
XMLHttpRequest(XHR)
XHR允许一步发送和接收数据,可以指定和读取头信息,并且监听请求的状态,达到精确控制数据的请求和接收,但是不能跨域请求数据。
var url = '/data.php'; //请求地址
var params = [ //参数
'id=934875',
'limit=20'
];
var req = new XMLHttpRequest(); // 创建XHR(标准方式)
req.onreadystatechange = function() {
if (req.readyState === 4) { // 所有数据接受完毕
var responseHeaders = req.getAllResponseHeaders(); // 获取响应头信息
var data = req.responseText; // 获取数据
// 处理数据
}
}
req.open('GET', url + '?' + params.join('&'), true);
req.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); // 设置请求的头信息
req.send(null); // 发送请求
readyState是XHR优化的关键,可以在readyState==3,即数据开始接收但未完成时(这就是流streaming),对接收到的数据提前进行处理,这也是Multipart XHR的基础。
if (req.readyState === 3) { // 数据开始接收,但未完成
var dataSoFar = req.responseText;
...
}
else if (req.readyState === 4) { // 所有数据接受完毕
var data = req.responseText;
...
}
仅仅请求数据时,使用get方式的XHR请求,可以缓存数据提高性能,只有请求的参数很长,超过2048字节(ie的url长度限制)时,才使用POST。
动态脚本注入(Dynamic script tag insertion)
这种方法也叫JsonP,可以实现跨域请求数据,但是必须通过回调函数和服务器端返回的数据进行对接,把数据直接作为一个参数传入,无需对数据进行预处理;这样不能读取和设置头信息,只能通过get的方式发送请求,也不能监听请求的状态,引入无法控制的外部数据时需要小心处理。
//在客户端创建异步请求
var scriptElement = document.createElement('script');
scriptElement.src = 'http://any-domain.com/javascript/lib.js';
document.getElementsByTagName('head')[0].appendChild(scriptElement);
//在客户端定义回调函数
function jsonCallback(jsonString) {
var data = eval('(' + jsonString + ')');
// Process the data here...
}
//在服务器端把数据封装在回调函数中
http://any-domain.com/javascript/lib.js
jsonCallback({ "status": 1, "colors": [ "#fff", "#000", "#ff0000" ] });
Multipart XHR
MXHR允许在一个异步请求中传输多个文件(限于文本,js、css和base64图片),过程和XHR类似,只是在服务器端对数据进行了转换和合并(用约定字符串分隔),在客户端对接收到的数据进行监听和处理,这能够减少页面请求数,提升性能。
MXHR依赖定时器和分隔符,简单过程请看下面的代码,健壮的MXHR和性能测试可参考。
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 = length;
}
MXHR的缺点是不能缓存;且依赖于readyState==3和base64,ie6、7需要单独处理。
服务器端用php来合并资源(图片)
//读取图片并转化为base64编码的字符串
$images = array('kitten.jpg', 'sunset.jpg', 'baby.jpg');
foreach ($images as $image) {
$image_fh = fopen($image, 'r');
$image_data = fread($image_fh, filesize($image));
fclose($image_fh);
$payloads[] = base64_encode($image_data);
}
// 合并成一个字符串输出
$newline = chr(1); // 该字符不会出现在任何base64字符串中
echo implode($newline, $payloads);
客户端取得字符串之后转化为可用资源
function splitImages(imageString) {
var imageData = imageString.split("\u0001");
var imageElement;
for (var i = 0, len = imageData.length; i < len; i++) {
imageElement = document.createElement('img');
imageElement.src = 'data:image/jpeg;base64,' + imageData[i];
document.getElementById('container').appendChild(imageElement);
}
}
function handleImageData(data, mimeType) {
var img = document.createElement('img');
img.src = 'data:' + mimeType + ';base64,' + data;
return img;
}
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);
}
function handleJavaScript(data) {
eval(data);
}
发送数据
XHR发送数据
XHR发送数据和请求数据同样处理,使用POST方式发送数据时,至少会发送2个数据包。一个头信息、一个正文,而get方式只会发送正文一个数据包。
function xhrPost(url, params, callback) {
var req = new XMLHttpRequest();
req.onerror = function() {
setTimeout(function() {
xhrPost(url, params, callback);
}, 1000);
};
req.onreadystatechange = function() {
if (req.readyState == 4) {
if (callback && typeof callback === 'function') {
callback();
}
}
};
req.open('POST', url, true);// POST时需要注意header的处理
req.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
req.setRequestHeader('Content-Length', params.length);
req.send(params.join('&'));
}
信标 Beacons
信标和脚本注入类似,只不过这里是创建一个image对象,把image的src设置为服务器脚本的url,但是并不把image插入DOM,在你只需要发送少量数据时使用。
可以有简单的监听,如果不需要返回信息时,发送不带正文的204 no content状态码,来阻止客户端继续等待正文。
var url = '/status_tracker.php';
var params = [
'step=2',
'time=1248027314'
];
var beacon = new Image();
beacon.src = url + '?' + params.join('&');
//服务器返回图片的宽度值来表示服务器的状态
beacon.onload = function() {
if (this.width == 1) {
// 成功
}
else if (this.width == 2) {
// 失败 继续尝试
}
};
beacon.onerror = function() {
// 容错处理
};
数据格式
数据格式对性能的影响体现在2个方面:下载(体积)、解析,因此需要根据具体情况来选用适用的数据格式。
XML格式
XML数据的体积比较庞大,结构依赖性强,语法模糊,而且解析的花费也较大,在高性能ajax中没有立足之地。
标准XML
<?xml version="1.0" encoding='UTF-8'?>
<users total="4">
<user id="1">
<username>alice</username>
<realname>Alice Smith</realname>
<email>alice@alicesmith.com</email>
</user>
<user id="2">
<username>bob</username>
<realname>Bob Jones</realname>
<email>bob@bobjones.com</email>
</user>
</users>
简化的XML,用属性代替子标签
<?xml version="1.0" encoding='UTF-8'?>
<users total="4">
<user id="1-id001" username="alice" realname="Alice Smith" email="alice@alicesmith.com" />
<user id="2-id001" username="bob" realname="Bob Jones" email="bob@bobjones.com" />
</users>
XML数据解析
function parseXML(responseXML) {
var users = [];
var userNodes = responseXML.getElementsByTagName('users');
for (var i = 0, len = userNodes.length; i < len; i++) {
users[i] = {
id: userNodes[i].getAttribute('id'),
username: userNodes[i].getAttribute('username'),
realname: userNodes[i].getAttribute('realname'),
email: userNodes[i].getAttribute('email')
};
}
return users;
}
JSON
JSON使用javascript对象和数组直接量,因此体积小,而且易于解析。http://www.json.org/
JSON数据
[
{ "id": 1, "username": "alice", "realname": "Alice Smith", "email": "alice@alicesmith.com" },
{ "id": 2, "username": "bob", "realname": "Bob Jones", "email": "bob@bobjones.com" }
]
解析JSON数据
function parseJSON(responseText) {
return eval('(' + responseText + ')');
}
不推荐使用eval来处理,可用JSON.parse()替代,安全性和容错性更好。
简化版JSON
[
{ "i": 1, "u": "alice", "r": "Alice Smith", "e": "alice@alicesmith.com" },
{ "i": 2, "u": "bob", "r": "Bob Jones", "e": "bob@bobjones.com" }
]
数组JSON
[
[ 1, "alice", "Alice Smith", "alice@alicesmith.com" ],
[ 2, "bob", "Bob Jones", "bob@bobjones.com" ]
]
解析数组json
function parseJSON(responseText) {
var users = [];
var usersArray = eval('(' + responseText + ')');
for (var i = 0, len = usersArray.length; i < len; i++) {
users[i] = {
id: usersArray[i][0],
username: usersArray[i][1],
realname: usersArray[i][2],
email: usersArray[i][3]
};
}
return users;
}
数组JSON体积和解析都是最好的,但是数据的可读性变差了,也更加脆弱,对数据结构和具体内容要求更严谨。
JSON-P
JSON填充(JSON with padding),简称JSON-P,即把JSON数据作为参数,封装在一个回调函数里,数据是原生javascript,无需解析,不需要像JSON数据需要先JSON.parse或者eval处理,因此解析速度可以达到XHR的10倍,动态脚本注入结合JSON-P是高性能Ajax的基础。
parseJSON([
{ "id": 1, "username": "alice", "realname": "Alice Smith", "email": "alice@alicesmith.com" },
{ "id": 2, "username": "bob", "realname": "Bob Jones", "email": "bob@bobjones.com" }
]);
不要把敏感的数据放在JSON-P中,因为任何网站都可以调用这些数据,无法完全控制。
HTML
如果返回的数据是HTML格式的,在服务器端把数据转化为HTML,这比在客户端用javascript解析更快,那么页面就无需解析,直接用innerHTML插入到页面对应位置即可。
<ul class="users">
<li class="user" id="1-id002">
<a href="http://www.site.com/alice/" class="username">alice</a>
<span class="realname">Alice Smith</span>
<a href="mailto:alice@alicesmith.com"class="email">alice@alicesmith.com</a>
</li>
<li class="user" id="2-id002">
<a href="http://www.site.com/bob/" class="username">bob</a>
<span class="realname">Bob Jones</span>
<a href="mailto:bob@bobjones.com" class="email">bob@bobjones.com</a>
</li>
</ul>
HTML格式的数据比较臃肿,结构更复杂,调整结构时需要在服务器端修改,和json格式相反,HTML在下载中耗时较多,页面解析时消耗的CPU会减少,但还是需要较长时间来解析,其性能与其他格式无法直接比较,因此只有在极端情况下推荐使用。
自定义格式
理想的数据应该只包含尽量少的结构,同时可以识别出其中的每一个独立字段,对于非常大的数据这种格式是最快的,在解析速度和加载时间上。
1:alice:Alice Smith:alice@alicesmith.com;
2:bob:Bob Jones:bob@bobjones.com
function parseCustomFormat(responseText) {
var users = [];
var usersEncoded = responseText.split(';');
var userArray;
for (var i = 0, len = usersEncoded.length; i < len; i++) {
userArray = usersEncoded[i].split(':');
users[i] = {
id: userArray[0],
username: userArray[1],
realname: userArray[2],
email: userArray[3]
};
}
return users;
}
然后通过split或循环split来处理数据,在javascript中split和循环处理这些字符串数据是非常快的。这里分隔符的选用是最重要的,理想情况下,她应该是一个单字符,而且不会在数据中出现。
PHP中使用ASCII字符示例:
function build_format_custom($users) {
$row_delimiter = chr(1); // \u0001 javascript中的Unicode编码
$field_delimiter = chr(2); // \u0002 javascript中的Unicode编码
$output = array();
foreach ($users as $user) {
$fields = array($user['id'], $user['username'], $user['realname'], $user['email']);
$output[] = implode($field_delimiter, $fields);
}
return implode($row_delimiter, $output);
}
split可以使用字符串和正则作为参数,如果数据中存在空白记录,使用字符串比较保险,如果用正则,在ie中,split会忽略紧挨着的2个分隔符中的第二个分隔符,作者steven levithan有专门的解决方案。
// 正则作为分割参数
var rows = req.responseText.split(/\u0001/);
// 正则作为分割参数(更安全)
var rows = req.responseText.split("\u0001");
Ajax性能优化
在选择了合适的传输方式和数据结构之后,可以继续从缓存来优化Ajax。
在服务器端设置HTTP头信息确保数据被浏览器缓存,然后使用get方式来获取数据,这是首选方案,可以跨页面和会话(sessions)。
PHP设置代码,缓存7天
$lifetime = 7 * 24 * 60 * 60; // 7天,以秒为单位
header('Expires: ' . gmdate('D, d M Y H:i:s', time() + $lifetime) . ' GMT');
在客户端把数据存储到本地,避免再次请求,这可以很好的控制缓存,在你需要更新缓存时可以删除缓存,此外移动设备上浏览器缓存很小时,这也是最佳选择。
var localCache = {};
function xhrRequest(url, callback) {
// 检查请求是否已有本地缓存,有缓存则直接利用
if (localCache[url]) {
callback.success(localCache[url]);
return;
}
// 如果没有缓存,发送请求
var req = createXhrObject();
req.onerror = function() {
callback.error();
};
req.onreadystatechange = function() {
if (req.readyState == 4) {
if (req.responseText === '' || req.status == '404') {
callback.error();
return;
}
// 存储放回数据到本地缓存
localCache[url] = req.responseText;
callback.success(req.responseText);
}
};
req.open("GET", url, true);
req.send(null);
}
//必要时删除缓存
delete localCache['/user/friendlist/'];
Ajax库的局限
所有的javascript库都有自己的ajax对象,统一的接口,很好的兼容性。但是不能访问XMLHttpRequest的完整功能,比如MXHR中对readyState值为3的监听,直接操作XHR可以减少开销,但是也会带来一下兼容性问题。
function createXhrObject() {
var msxml_progid = [
'MSXML2.XMLHTTP.6.0',
'MSXML3.XMLHTTP',
'Microsoft.XMLHTTP', // 不支持readyState 3.
'MSXML2.XMLHTTP.3.0', // 不支持readyState 3.
];
var req;
try {
req = new XMLHttpRequest(); // 先尝试标准方法
}
catch(e) {
for (var i = 0, len = msxml_progid.length; i < len; ++i) {
try {
req = new ActiveXObject(msxml_progid[i]);
break;
}
catch(e2) { }
}
}
finally {
return req;
}
}
tips