广告位 · 页头(后台-模块管理-新建模块后替换此区域)

解决IE9中jQuery.ajax无法处理responseType为arraybuffer的二进制数据流

解决IE9中jQuery.ajax无法处理responseType为arraybuffer的二进制数据流

在需要处理音频、图像、压缩包等二进制数据的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缓存旧数据导致内容不更新。

广告位 · 文章底部(后台-模块管理替换此区域)