
在需要处理音频、图像、压缩包等二进制数据的Web应用里,前端通过AJAX请求服务端接口来获取原始字节流是一种常见做法。现代浏览器中,使用jQuery.ajax并设置xhrFields里的responseType为arraybuffer,几乎可以无脑实现。但同样的代码一旦运行在IE9上,就会出现数据解析异常:明明服务端返回的是二进制,回调参数却变成了乱码字符串,后续的Blob构造或TypedArray操作全部失败。这个问题的根源并不在jQuery本身,而在于IE9对XMLHttpRequest接口的实现程度远低于现代标准。下面从现象入手,分析底层原因,并给出两种可落地的解决方案。
为什么设置responseType会失效
先来看一段常见的jQuery调用代码:
$.ajax({
url: '/download/file.bin',
method: 'GET',
xhrFields: {
responseType: 'arraybuffer'
},
success: function(data) {
// 在IE9中,这里的data并不是ArrayBuffer,而是字符串
var byteLength = data.byteLength; // 报错:byteLength为undefined
}
});
这段代码在Chrome或Firefox里运行正常,data会是一个包含原始字节的ArrayBuffer对象。但在IE9里,success回调收到的data却是一个字符串,length可能等于字节数,但内容已经被按UTF-8或系统默认编码解释过了,导致二进制数据中的某些字节组合被替换或丢失。比如一个JPEG文件的头部0xFF 0xD8,经过字符串转换后可能变成两个Unicode字符,文件结构彻底损坏。
核心原因在于IE9实现的XMLHttpRequest对象没有遵循W3C的XMLHttpRequest Level 2规范。规范中规定responseType属性可以设置为arraybuffer、blob、document、json、text等值,并且请求完成后可以通过response属性读取对应类型的数据。但IE9只实现了最基础的Level 1接口,它提供了responseText、responseXML和一个非标准的responseBody。responseBody返回的是一个VBArray对象,该对象表示原始字节流,但没有暴露为JavaScript可直接使用的ArrayBuffer。当jQuery内部尝试根据responseType去读取xhr.response时,发现该属性不存在,于是退回到responseText,用字符串方式包装数据,这就是失效的直接原因。
还有一个细节值得注意:即使手动使用原生XHR,在IE9中给xhr.responseType赋值也不会报错,但赋值操作会被忽略。因为IE9的XHR对象并没有实现responseType的setter,它只是一个普通属性,赋值后没有任何实际效果。所以检测浏览器是否支持responseType不能靠判断属性是否存在,而需要实际赋值后读取比对,或者通过更可靠的能力检测。
使用原生XHR和VBArray手动转换
既然jQuery在IE9下无法正确处理二进制响应,最直接的方案就是绕开jQuery,针对IE9单独使用原生XMLHttpRequest,并手动将VBArray转换为ArrayBuffer。现代浏览器仍可继续使用标准responseType方式,所以需要写一个兼容分支。
IE9的XMLHttpRequest对象在请求完成后,可以通过xhr.responseBody获取VBArray形式的原始字节。VBArray是微软特有的数据集合,它提供了toArray()方法将内容转成一个普通的JavaScript数组,数组元素为0到255之间的整数。得到普通数组后,可以创建Uint8Array,再将其底层缓冲取出作为ArrayBuffer,或者直接构造ArrayBuffer并逐字节填充。下面是一段完整的兼容代码:
function loadBinary(url, callback, errorCallback) {
var xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
// 现代浏览器支持responseType
if ('responseType' in xhr) {
try {
xhr.responseType = 'arraybuffer';
} catch (e) {
// 忽略赋值失败的情况
}
}
xhr.onreadystatechange = function() {
if (xhr.readyState !== 4) return;
if (xhr.status !== 200) {
if (errorCallback) errorCallback(new Error('HTTP error: ' + xhr.status));
return;
}
var arrayBuffer;
if (xhr.response) {
// 标准路径:response已经是ArrayBuffer
arrayBuffer = xhr.response;
} else if (xhr.responseBody !== undefined) {
// IE9专用路径:从VBArray转换
var vbArray = xhr.responseBody;
var byteArray = new VBArray(vbArray).toArray();
arrayBuffer = new ArrayBuffer(byteArray.length);
var uint8 = new Uint8Array(arrayBuffer);
for (var i = 0; i < byteArray.length; i++) {
uint8[i] = byteArray[i] & 0xFF;
}
} else {
if (errorCallback) errorCallback(new Error('Unsupported response type'));
return;
}
callback(arrayBuffer);
};
xhr.onerror = function() {
if (errorCallback) errorCallback(new Error('Network error'));
};
xhr.send(null);
}
这个函数的核心在于判断xhr.response是否存在。在IE9中,虽然给xhr.responseType赋值了arraybuffer,但response属性根本不存在,所以会走到responseBody分支。new VBArray(vbArray)可以安全地将VBArray包装,toArray返回的是数组,但需要注意数组长度可能很大,对于大型文件逐字节循环会产生一定性能开销,不过对于IE9这类老旧环境,通常请求的数据量不会特别夸张,可以接受。
转换后得到的ArrayBuffer可以直接用于FileReader、Blob构造(IE9不支持Blob,需降级为base64或直接使用ArrayBuffer)、或者Web Audio API的decodeAudioData等。如果还需要将ArrayBuffer转成Base64以便在img标签中显示,可以再写一个转换函数,但这不是本文重点。使用原生XHR后,jQuery的优势如统一的promise、错误处理、超时设置等就需要自己补上,所以适合在代码量少、仅个别请求需要二进制的场景。
通过ajaxTransport保持jQuery调用方式不变
如果项目里大量使用jQuery.ajax,并且不想为二进制请求单独写原生XHR,可以利用jQuery提供的ajaxTransport扩展机制。jQuery允许开发者注册自定义传输工厂,在发起请求时,jQuery会遍历所有已注册的transport,找到第一个匹配并返回一个包含send和abort方法的对象。这样就能在保持$.ajax调用格式不变的前提下,完全接管请求的底层实现。
具体思路是:注册一个transport,检查当前请求的设置中是否要求返回arraybuffer,并且当前浏览器是否为IE9及以下。如果是,则返回一个自定义transport对象。在send方法中,手动创建XMLHttpRequest,按照上一节的VBArray转换逻辑获取数据,最后通过调用jQuery提供的成功回调(即options.success)把ArrayBuffer作为data参数传回去。同时还需要处理状态码、超时、错误等回调,模拟jQuery的标准行为。
(function($) {
// 检测是否需要使用自定义二进制transport
function isIENeedsBinaryTransport(options) {
return options.dataType === 'arraybuffer' ||
(options.xhrFields && options.xhrFields.responseType === 'arraybuffer');
}
$.ajaxTransport('+arraybuffer', function(options, originalOptions, jqXHR) {
if (!isIENeedsBinaryTransport(originalOptions)) {
return; // 交给标准transport处理
}
// 仅IE9及以下需要特殊处理
var ieVersion = getIEVersion();
if (ieVersion === undefined || ieVersion > 9) {
return;
}
return {
send: function(headers, completeCallback) {
var xhr = new XMLHttpRequest();
xhr.open(options.type, options.url, options.async);
// 复制headers
for (var key in headers) {
xhr.setRequestHeader(key, headers[key]);
}
xhr.onreadystatechange = function() {
if (xhr.readyState !== 4) return;
var status = xhr.status;
var statusText = xhr.statusText || '';
var responseHeaders = {};
var rawHeaders = xhr.getAllResponseHeaders();
if (rawHeaders) {
var headerLines = rawHeaders.split('\n');
for (var i = 0; i < headerLines.length; i++) {
var line = headerLines[i].trim();
if (!line) continue;
var idx = line.indexOf(':');
if (idx > -1) {
var name = line.substring(0, idx).trim().toLowerCase();
var value = line.substring(idx + 1).trim();
responseHeaders[name] = value;
}
}
}
var responseData;
if (xhr.responseBody) {
var byteArray = new VBArray(xhr.responseBody).toArray();
var arrayBuffer = new ArrayBuffer(byteArray.length);
var uint8 = new Uint8Array(arrayBuffer);
for (var j = 0; j < byteArray.length; j++) {
uint8[j] = byteArray[j] & 0xFF;
}
responseData = arrayBuffer;
} else {
responseData = xhr.responseText;
}
completeCallback(status, statusText, { arraybuffer: responseData }, responseHeaders);
};
xhr.onerror = function() {
completeCallback(xhr.status || 0, 'error', {}, {});
};
xhr.send(options.data || null);
},
abort: function() {
// 可通过闭包引用xhr并中止,此处简化处理
}
};
});
function getIEVersion() {
var ua = navigator.userAgent;
var match = ua.match(/(?:MSIE |Trident\/.*; rv:)(\d+)/);
return match ? parseInt(match[1], 10) : undefined;
}
})(jQuery);
这段代码注册了一个名为+arraybuffer的transport。前面的加号表示该transport只处理dataType为arraybuffer的请求,jQuery会优先匹配更具体的transport。在send方法里,我们手动创建XHR,设置请求头,然后在readystatechange中处理响应。completeCallback是jQuery内部用来触发done/fail回调的函数,第一个参数是状态码,第二个是状态文本,第三个是一个对象,键表示数据类型,值是对应的数据。这里我们键写arraybuffer,值就是转换好的ArrayBuffer。这样jQuery的success回调中的data参数就会直接是ArrayBuffer。
需要注意的是,请求超时、abort等逻辑在自定义transport中也需要手动实现,否则jqXHR的timeout和abort设置不会自动生效。上面的示例为了简洁省略了超时和abort的完整处理,实际项目中应该把这些细节补全。另外,getAllResponseHeaders在IE9下可用,但返回的headers格式与标准略有差异,解析时需要做兼容。这个方案的优势在于业务代码可以完全不改动,所有$.ajax调用保持原样,只是增加了一段transport注册代码。
服务端Base64传输与降级策略
如果不想在前端写复杂的VBArray转换,也可以考虑由服务端配合,将二进制数据用Base64编码后作为纯文本返回,前端再用atob或其他解码函数还原成字节流。这种方案对任何浏览器都通用,尤其适合老式IE环境。Base64编码后的数据体积比原始二进制大约增加三分之一,对于小文件或对流量不敏感的场景完全可行。前端拿到Base64字符串后,可以通过循环解码每个字符的字符码来构造Uint8Array,再生成ArrayBuffer。
function base64ToArrayBuffer(base64) {
var binaryString = atob(base64);
var len = binaryString.length;
var bytes = new Uint8Array(len);
for (var i = 0; i < len; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes.buffer;
}
这个函数依赖全局的atob,它在IE9中可用,但需要注意处理换行符和空格。如果Base64字符串中包含空格或换行,需要先清理。另一种方式是不使用atob,而是手动解析Base64字符串,这样可以兼容更老的环境,但代码量稍大。服务端返回Base64时,jQuery.ajax只需设置dataType为text,没有任何兼容性问题。这种方案的缺点也很明显:数据膨胀、客户端需要额外的解码耗时、以及服务端需要增加编码逻辑。但作为兜底策略,当其他方案都无法实施时,它是最稳定的选择。
在真实的业务场景中,通常会根据用户浏览器版本做一个能力检测:如果支持responseType,走标准路径;如果不支持但存在responseBody,走VBArray转换;如果连XHR都不支持,再退化为Base64文本。这种分层策略可以覆盖从IE6到最新Chrome的所有环境。无论选择哪种方案,关键是要清楚数据在传输链路中的实际类型,避免在回调中对错误的类型做操作。另外要警惕IE9的缓存问题:如果使用GET请求二进制数据,最好在URL后加上随机参数或设置缓存控制头,防止IE缓存旧数据导致内容不更新。

