如何从 IE 中的 Javascript 访问 XHR responseBody(用于二进制数据)?

IT技术 javascript xmlhttprequest
2021-01-26 16:22:24

我有一个使用XMLHttpRequest下载二进制资源的网页

在 Firefox 和 Gecko 中,即使字节流包含二进制零,我也可以使用 responseText 来获取字节。我可能需要强制使用 mimetypeoverrideMimeType()来实现这一点。但是,在 IE 中, responseText 不起作用,因为它似乎在第一个零处终止。如果您读取 100,000 个字节,并且字节 7 是二进制零,则您将只能访问 7 个字节。IE 的 XMLHttpRequest 公开了一个responseBody访问字节属性。我已经看到一些帖子表明不可能直接从 Javascript 以任何有意义的方式访问这个属性。这对我来说听起来很疯狂。

xhr.responseBody 从VBScript访问,因此明显的解决方法是在VBScript在网页中定义的方法,然后从JavaScript调用该方法。参见jsdap的一个例子。 编辑:不要使用这个 VBScript !!

var IE_HACK = (/msie/i.test(navigator.userAgent) && 
               !/opera/i.test(navigator.userAgent));   

// no no no!  Don't do this! 
if (IE_HACK) document.write('<script type="text/vbscript">\n\
     Function BinaryToArray(Binary)\n\
         Dim i\n\
         ReDim byteArray(LenB(Binary))\n\
         For i = 1 To LenB(Binary)\n\
             byteArray(i-1) = AscB(MidB(Binary, i, 1))\n\
         Next\n\
         BinaryToArray = byteArray\n\
     End Function\n\
</script>'); 

var xml = (window.XMLHttpRequest) 
    ? new XMLHttpRequest()      // Mozilla/Safari/IE7+
    : (window.ActiveXObject) 
      ? new ActiveXObject("MSXML2.XMLHTTP")  // IE6
      : null;  // Commodore 64?


xml.open("GET", url, true);
if (xml.overrideMimeType) {
    xml.overrideMimeType('text/plain; charset=x-user-defined');
} else {
    xml.setRequestHeader('Accept-Charset', 'x-user-defined');
}

xml.onreadystatechange = function() {
    if (xml.readyState == 4) {
        if (!binary) {
            callback(xml.responseText);
        } else if (IE_HACK) {
            // call a VBScript method to copy every single byte
            callback(BinaryToArray(xml.responseBody).toArray());
        } else {
            callback(getBuffer(xml.responseText));
        }
    }
};
xml.send('');

这是真的吗?最好的方法?复制每个字节?对于效率不会很高的大型二进制流。

还有一种可能的技术是使用 ADODB.Stream,它是 MemoryStream 的 COM 等价物。 有关示例,请参见此处它不需要 VBScript,但需要一个单独的 COM 对象。

if (typeof (ActiveXObject) != "undefined" && typeof (httpRequest.responseBody) != "undefined") {
    // Convert httpRequest.responseBody byte stream to shift_jis encoded string
    var stream = new ActiveXObject("ADODB.Stream");
    stream.Type = 1; // adTypeBinary
    stream.Open ();
    stream.Write (httpRequest.responseBody);
    stream.Position = 0;
    stream.Type = 1; // adTypeBinary;
    stream.Read....          /// ???? what here
}

但这不会很好地工作 - 现在大多数机器上都禁用了 ADODB.Stream。


在 IE8 开发人员工具中——IE 相当于 Firebug——我可以看到 responseBody 是一个字节数组,我什至可以看到字节本身。数据就在那里我不明白为什么我不能得到它。

我可以用 responseText 阅读它吗?

提示?(除了定义 VBScript 方法)

6个回答

是的,我想出的在 IE 中通过 XHR 读取二进制数据的答案是使用 VBScript 注入。起初这让我很反感,但是,我将其视为又一个依赖于浏览器的代码。(常规 XHR 和 responseText 在其他浏览器中工作正常;您可能必须使用 强制 mime 类型XMLHttpRequest.overrideMimeType()。这在 IE 上不可用)。

这就是我如何得到一个responseText在 IE中工作的东西,即使对于二进制数据也是如此。首先,一次性注入一些 VBScript,如下所示:

if(/msie/i.test(navigator.userAgent) && !/opera/i.test(navigator.userAgent)) {
    var IEBinaryToArray_ByteStr_Script =
    "<!-- IEBinaryToArray_ByteStr -->\r\n"+
    "<script type='text/vbscript' language='VBScript'>\r\n"+
    "Function IEBinaryToArray_ByteStr(Binary)\r\n"+
    "   IEBinaryToArray_ByteStr = CStr(Binary)\r\n"+
    "End Function\r\n"+
    "Function IEBinaryToArray_ByteStr_Last(Binary)\r\n"+
    "   Dim lastIndex\r\n"+
    "   lastIndex = LenB(Binary)\r\n"+
    "   if lastIndex mod 2 Then\r\n"+
    "       IEBinaryToArray_ByteStr_Last = Chr( AscB( MidB( Binary, lastIndex, 1 ) ) )\r\n"+
    "   Else\r\n"+
    "       IEBinaryToArray_ByteStr_Last = "+'""'+"\r\n"+
    "   End If\r\n"+
    "End Function\r\n"+
    "</script>\r\n";

    // inject VBScript
    document.write(IEBinaryToArray_ByteStr_Script);
}

我正在使用的读取二进制文件的 JS 类公开了一个有趣的方法,readCharAt(i),它读取第 i 个索引处的字符(实际上是一个字节)。我是这样设置的:

// see doc on http://msdn.microsoft.com/en-us/library/ms535874(VS.85).aspx
function getXMLHttpRequest() 
{
    if (window.XMLHttpRequest) {
        return new window.XMLHttpRequest;
    }
    else {
        try {
            return new ActiveXObject("MSXML2.XMLHTTP"); 
        }
        catch(ex) {
            return null;
        }
    }
}

// this fn is invoked if IE
function IeBinFileReaderImpl(fileURL){
    this.req = getXMLHttpRequest();
    this.req.open("GET", fileURL, true);
    this.req.setRequestHeader("Accept-Charset", "x-user-defined");
    // my helper to convert from responseBody to a "responseText" like thing
    var convertResponseBodyToText = function (binary) {
        var byteMapping = {};
        for ( var i = 0; i < 256; i++ ) {
            for ( var j = 0; j < 256; j++ ) {
                byteMapping[ String.fromCharCode( i + j * 256 ) ] =
                    String.fromCharCode(i) + String.fromCharCode(j);
            }
        }
        // call into VBScript utility fns
        var rawBytes = IEBinaryToArray_ByteStr(binary);
        var lastChr = IEBinaryToArray_ByteStr_Last(binary);
        return rawBytes.replace(/[\s\S]/g,
                                function( match ) { return byteMapping[match]; }) + lastChr;
    };

    this.req.onreadystatechange = function(event){
        if (that.req.readyState == 4) {
            that.status = "Status: " + that.req.status;
            //that.httpStatus = that.req.status;
            if (that.req.status == 200) {
                // this doesn't work
                //fileContents = that.req.responseBody.toArray(); 

                // this doesn't work
                //fileContents = new VBArray(that.req.responseBody).toArray(); 

                // this works...
                var fileContents = convertResponseBodyToText(that.req.responseBody);

                fileSize = fileContents.length-1;
                if(that.fileSize < 0) throwException(_exception.FileLoadFailed);
                that.readByteAt = function(i){
                    return fileContents.charCodeAt(i) & 0xff;
                };
            }
            if (typeof callback == "function"){ callback(that);}
        }
    };
    this.req.send();
}

// this fn is invoked if non IE
function NormalBinFileReaderImpl(fileURL){
    this.req = new XMLHttpRequest();
    this.req.open('GET', fileURL, true);
    this.req.onreadystatechange = function(aEvt) {
        if (that.req.readyState == 4) {
            if(that.req.status == 200){
                var fileContents = that.req.responseText;
                fileSize = fileContents.length;

                that.readByteAt = function(i){
                    return fileContents.charCodeAt(i) & 0xff;
                }
                if (typeof callback == "function"){ callback(that);}
            }
            else
                throwException(_exception.FileLoadFailed);
        }
    };
    //XHR binary charset opt by Marcus Granado 2006 [http://mgran.blogspot.com] 
    this.req.overrideMimeType('text/plain; charset=x-user-defined');
    this.req.send(null);
}

转换码被提供Miskun。

非常快,效果很好。

我使用这种方法从 Javascript 中读取和提取 zip 文件,也在一个用 Javascript 读取和显示 EPUB 文件的类中使用。非常合理的表现。一个 500kb 的文件大约需要半秒。

但是……那么这可能是一个不同的问题:IE 不允许[]对字符串进行操作。它使用.charAt(num)代替。所以它可能只是你需要的responseText.charAt(i)
2021-03-20 16:22:24
你用什么在其他浏览器中读取二进制数据?我看到你的问题引用了一个getBuffer函数。那是什么?AFAIK 仅 IE 支持responseBody. 你用的是什么??
2021-03-28 16:22:24
谢谢你。现在我要洗个澡……我觉得好脏。但很高兴这解决了我的问题。
2021-03-28 16:22:24
我使用 responseText.charCodeAt(i) - 它在 IE 中不起作用,在第一个零之后。我刚刚用更多代码更新了帖子,以进行说明。
2021-03-30 16:22:24
我在 IE 中使用 MSXML2.XMLHTTP,在非 IE 中使用 XMLHttpRequest()。在非 IE 浏览器中,我能够使用 responseText 来获取字节流。显然 IE 认为它是一个字符串,因此在 IE 中,除了第一个零之外,responseText[i] 返回“未定义”。但 FF3.5 并非如此。它只是有效。
2021-04-11 16:22:24

XMLHttpRequest.responseBody是一个VBArray包含原始字节对象。您可以使用以下toArray()函数将这些对象转换为标准数组

var data = xhr.responseBody.toArray();
再试一次,我刚刚确认它对我来说按预期工作。应该只是替换您正在执行的操作: var fileContents = BinaryToArray(that.req.responseBody).toArray();
2021-03-15 16:22:24
@timrice:“VBArray”方法不适用于 IE8 或更早版本。它会抛出“VBArray 预期”类型错误。特征检测也不起作用,因为“VBArray”对象本身存在于 IE8 上。请停止盲目传播有缺陷的方法。虽然它适用于 IE9 及更高版本(即使在 IE8 模式下),但它并不比“VBScript+CStr”方法快。
2021-03-16 16:22:24
还应该考虑进行特征检测而不是查看用户代理字符串。只需检查“overridemimetype”是否存在,如果不存在,请检查 VBArray 是否存在,如果不存在,则平底船;)
2021-03-24 16:22:24
我相信我试过了,但没有奏效;不过现在想不起来是什么问题了。
2021-04-05 16:22:24
谢谢,在 IE9 中对我很有用(无论如何我支持的最低 IE 版本),并且是一个更简单的解决方案。
2021-04-12 16:22:24

我建议另外两个(快速)选项:

  1. 首先,您可以使用 ADODB.Recordset将字节数组转换为字符串。我猜想这个对象比 ADODB.Stream 更常见,出于安全原因,它经常被禁用。此选项非常快,对于 500kB 文件不到 30毫秒

  2. 其次,如果 Recordset 组件不可访问,则有一个技巧可以从 Javascript 访问字节数组数据将您的 xhr.responseBody 发送到 VBScript,将它传递给任何 VBScript 字符串函数,例如 CStr(不花时间),然后将其返回给 JS。你会得到一个奇怪的字符串,其中的字节连接成 16 位 unicode(反向)。然后,您可以通过带有基于字典的替换正则表达式将该字符串快速转换为可用的字节字符串500kB大约需要1秒。

为了进行比较,对于同一个 500kB 的文件,通过循环进行的逐字节转换需要几分钟,因此很容易 :) 在我一直使用的代码下方,插入到您的标题中。然后调用函数ieGetBytes你xhr.responseBody。

<!--[if IE]>    
<script type="text/vbscript">

    'Best case scenario when the ADODB.Recordset object exists
    'We will do the existence test in Javascript (see after)
    'Extremely fast, about 25ms for a 500kB file
    Function ieGetBytesADO(byteArray)
        Dim recordset
        Set recordset = CreateObject("ADODB.Recordset")
        With recordset
            .Fields.Append "temp", 201, LenB(byteArray)
            .Open
            .AddNew
            .Fields("temp").AppendChunk byteArray
            .Update
        End With
        ieGetBytesADO = recordset("temp")
        recordset.Close
        Set recordset = Nothing
    End Function

    'Trick to return a Javascript-readable string from a VBScript byte array
    'Yet the string is not usable as such by Javascript, since the bytes
    'are merged into 16-bit unicode characters. Last character missing if odd length.
    Function ieRawBytes(byteArray)
        ieRawBytes = CStr(byteArray)
    End Function

    'Careful the last character is missing in case of odd file length
    'We Will call the ieLastByte function (below) from Javascript
    'Cannot merge directly within ieRawBytes as the final byte would be duplicated
    Function ieLastChr(byteArray)
        Dim lastIndex
        lastIndex = LenB(byteArray)
        if lastIndex mod 2 Then
            ieLastChr = Chr( AscB( MidB( byteArray, lastIndex, 1 ) ) )
        Else
            ieLastChr = ""
        End If
    End Function

</script>

<script type="text/javascript">
    try {   
        // best case scenario, the ADODB.Recordset object exists
        // we can use the VBScript ieGetBytes function to transform a byte array into a string
        var ieRecordset = new ActiveXObject('ADODB.Recordset');
        var ieGetBytes = function( byteArray ) {
            return ieGetBytesADO(byteArray);
        }
        ieRecordset = null;

    } catch(err) {
        // no ADODB.Recordset object, we will do the conversion quickly through a regular expression

        // initializes for once and for all the translation dictionary to speed up our regexp replacement function
        var ieByteMapping = {};
        for ( var i = 0; i < 256; i++ ) {
            for ( var j = 0; j < 256; j++ ) {
                ieByteMapping[ String.fromCharCode( i + j * 256 ) ] = String.fromCharCode(i) + String.fromCharCode(j);
            }
        }

        // since ADODB is not there, we replace the previous VBScript ieGetBytesADO function with a regExp-based function,
        // quite fast, about 1.3 seconds for 500kB (versus several minutes for byte-by-byte loops over the byte array)
        var ieGetBytes = function( byteArray ) {
            var rawBytes = ieRawBytes(byteArray),
                lastChr = ieLastChr(byteArray);

            return rawBytes.replace(/[\s\S]/g, function( match ) {
                return ieByteMapping[match]; }) + lastChr;
        }
    }
</script>
<![endif]-->
ADODB 方法不起作用。尽管它可能很快,但它返回超出标准 ASCII 范围的字符。因此,您的数据最终会损坏。现在,如果有一种巧妙的方法可以将“字节数组”直接转换为 JS 数组(不使用 VBArray),那会更快。
2021-04-02 16:22:24
“通过循环逐字节转换”是否与此答案相对应
2021-04-13 16:22:24

非常感谢这个解决方案。VbScript 中的 BinaryToArray() 函数对我很有用。

顺便说一句,我需要将二进制数据提供给 Applet。(不要问我为什么 Applets 不能用于下载二进制数据。长话短说......奇怪的 MS 身份验证无法通过小程序 (URLConn) 调用。在用户位于代理后面的情况下尤其奇怪)

Applet 需要从这些数据中获取一个字节数组,所以我这样做是为了得到它:

 String[] results = result.toString().split(",");
    byte[] byteResults = new byte[results.length];
    for (int i=0; i<results.length; i++){
        byteResults[i] = (byte)Integer.parseInt(results[i]);
    }

然后可以将字节数组转换为 bytearrayinputstream 以进行进一步处理。

我试图下载一个文件,然后使用 CAPICOM.DLL 对其进行签名。我能做到的唯一方法是注入一个执行下载的 VBScript 函数。那是我的解决方案:

if(/msie/i.test(navigator.userAgent) && !/opera/i.test(navigator.userAgent)) {
    var VBConteudo_Script =
    '<!-- VBConteudo -->\r\n'+
    '<script type="text/vbscript">\r\n'+
    'Function VBConteudo(url)\r\n'+
    '   Set objHTTP = CreateObject("MSXML2.XMLHTTP")\r\n'+
    '   objHTTP.open "GET", url, False\r\n'+
    '   objHTTP.send\r\n'+
    '   If objHTTP.Status = 200 Then\r\n'+
    '       VBConteudo = objHTTP.responseBody\r\n'+
    '   End If\r\n'+
    'End Function\r\n'+
    '\<\/script>\r\n';

    // inject VBScript
    document.write(VBConteudo_Script);
}