как получить доступ к XHR responseBody (для двоичных данных) из Javascript в IE?

У меня есть веб-страница, использующая XMLHttpRequest. скачать бинарный ресурс.

В Firefox и Gecko я могу использовать responseText для получения байтов, даже если поток байтов включает двоичные нули. Чтобы это произошло, мне может понадобиться принудительно указать тип mimetype с помощью overrideMimeType(). В IE, тем не менее, responseText не работает, потому что кажется, что он заканчивается на первом нуле. Если вы прочитаете 100 000 байт, а байт 7 будет двоичным нулем, вы сможете получить доступ только к 7 байтам. XMLHttpRequest IE предоставляет свойство 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, который является COM-эквивалентом MemoryStream. см. здесь пример. Он не требует 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 — эквиваленте Firebug для IE — я вижу, что responseBody — это массив байтов, и я даже вижу сами байты. Данные здесь. Я не понимаю, почему я не могу добраться до него.

Могу ли я прочитать его с помощью responseText?

намеки? (кроме определения метода VBScript)


person Cheeso    schedule 17.12.2009    source источник


Ответы (7)


Да, ответ, который я придумал для чтения двоичных данных через XHR в IE, заключается в использовании инъекции 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.

Очень быстро, отлично работает.

Я использовал этот метод для чтения и извлечения zip-файлов из Javascript, а также в классе, который читает и отображает файлы EPUB в Javascript. Очень разумная производительность. Около полсекунды для файла размером 500 КБ.

person Cheeso    schedule 18.12.2009
comment
Что вы использовали для чтения двоичных данных в других браузерах? Я вижу, что ваш вопрос ссылается на функцию getBuffer. Это что? AFAIK только IE поддерживает responseBody. Итак, что вы использовали ?? - person Crescent Fresh; 18.12.2009
comment
Я использовал MSXML2.XMLHTTP в IE и XMLHttpRequest() не в IE. В браузерах, отличных от IE, я смог использовать responseText для получения потока байтов. Очевидно, IE считает, что это строка, и поэтому в IE после первого нуля responseText[i] возвращает значение undefined. Но это не так для FF3.5. Это просто работает. - person Cheeso; 18.12.2009
comment
Но... тогда это может быть другая проблема: IE не разрешает оператор [] для строк. Вместо этого используется .charAt(num). Так может быть вам просто нужно responseText.charAt(i)? - person Crescent Fresh; 18.12.2009
comment
Я использовал responseText.charCodeAt(i) - он не работал в IE после первого нуля. Я только что обновил сообщение с большим количеством кода, чтобы проиллюстрировать. - person Cheeso; 18.12.2009
comment
Спасибо. Теперь мне нужно принять ванну... Я чувствую себя таким грязным. Но так рад, что это решило мою проблему. - person Marius; 21.12.2012

XMLHttpRequest.responseBody — это объект VBArray, содержащий сырые байты. Вы можете преобразовать эти объекты в стандартные массивы, используя функцию toArray():

var data = xhr.responseBody.toArray();
person timrice    schedule 02.12.2010
comment
Кажется, я пробовал это, и это не сработало; не могу сейчас вспомнить, в чем была проблема. - person Cheeso; 02.12.2010
comment
Попробуйте еще раз, я только что подтвердил, что это работает, как и ожидалось. Должна быть просто замена того, что вы делаете: var fileContents = BinaryToArray(that.req.responseBody).toArray(); - person timrice; 02.12.2010
comment
Также следует рассмотреть возможность обнаружения функций вместо просмотра строки пользовательского агента. Просто проверьте, существует ли overridemimetype, если нет, проверьте, существует ли VBArray, если нет, плоскодонка;) - person timrice; 02.12.2010
comment
@timrice: метод VBArray не работает с IE8 и более ранними версиями. Это вызовет ожидаемую ошибку TypeError VBArray. Обнаружение функций также не работает, потому что сам объект VBArray существует в IE8. Пожалуйста, прекратите слепо распространять ошибочный метод. Хотя он работает с IE9 и более поздними версиями (даже в режиме IE8), он не настолько быстрее, чем метод VBScript+CStr. - person emk; 19.09.2011
comment
Спасибо, у меня отлично работает в IE9 (во всяком случае, моя минимальная поддерживаемая версия IE), и это гораздо более простое решение. - person Dave; 12.03.2012

Я бы предложил два других (быстрых) варианта:

  1. Во-первых, вы можете использовать ADODB.Recordset для преобразования массива байтов в строку. Я предполагаю, что этот объект более распространен, чем ADODB.Stream, который часто отключается из соображений безопасности. Этот вариант ОЧЕНЬ быстрый, менее 30 мс для файла размером 500 КБ.

  2. Во-вторых, если компонент Recordset недоступен, существует хитрость для доступа к данным массива байтов из Javascript. Отправьте свой xhr.responseBody в VBScript, передайте его через любую строковую функцию VBScript, например CStr (это не займет много времени), и верните его в JS. Вы получите странную строку с байтами, объединенными в 16-битный юникод (в обратном порядке). Затем вы можете быстро преобразовать эту строку в пригодную для использования строку байтов с помощью регулярного выражения с заменой на основе словаря. Занимает около 1 с для 500 КБ.

Для сравнения, побайтовое преобразование с помощью циклов занимает несколько минут для того же файла размером 500 КБ, так что это не проблема :) Ниже код, который я использовал, для вставки в ваш заголовок. Затем вызовите функцию 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]-->
person Louis LC    schedule 16.06.2010
comment
Метод ADODB не работает. Каким бы быстрым он ни был, он возвращает символы, выходящие за пределы стандартного диапазона ASCII. Таким образом, ваши данные в конечном итоге повреждены. Теперь, если бы был аккуратный способ конвертировать из массива байтов прямо в массив JS (без использования VBArray), это было бы быстрее. - person SineSwiper; 23.02.2012
comment
Соответствует ли побайтовое преобразование через циклы этому ответу? - person Knu; 03.09.2017

Большое спасибо за это решение. функция BinaryToArray() в VbScript отлично работает для меня.

Между прочим, мне нужны двоичные данные для передачи их в апплет. (Не спрашивайте меня, почему апплеты нельзя использовать для загрузки двоичных данных. Короче говоря.. странная аутентификация MS, которая не может проходить через вызовы апплетов (URLConn). Это особенно странно в случаях, когда пользователи находятся за прокси-сервером)

Аплету нужен массив байтов из этих данных, вот что я делаю, чтобы его получить:

 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]);
    }

Массив байтов затем может быть преобразован в байтовый поток ввода для дальнейшей обработки.

person rk2010    schedule 22.12.2009

Я пытался загрузить файл и подписать его с помощью 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);
}
person Renato Crivano    schedule 03.01.2014

Спасибо тебе за этот пост.

Я нашел эту ссылку полезной:

http://www.codingforums.com/javascript-programming/47018-help-using-responsetext-property-microsofts-xmlhttp-activexobject-ie6.html

Специально эта часть:

</script>
<script language="VBScript">
Function BinaryToString(Binary)
Dim I,S
For I = 1 to LenB(Binary)
S = S & Chr(AscB(MidB(Binary,I,1)))
Next
BinaryToString = S
End Function
</script>

Я добавил это на свою страницу htm. Затем я вызываю эту функцию из своего javascript:

 responseText = BinaryToString(xhr.responseBody);

Работает в IE8, IE9, IE10, FF и Chrome.

person George G    schedule 26.02.2014

Вы также можете просто создать прокси-скрипт, который идет по адресу, который вы запрашиваете, и это base64. Затем вам просто нужно передать строку запроса прокси-скрипту, который сообщает ему адрес. Однако в IE вам нужно вручную выполнить base64 в JS. Но это способ, если вы не хотите использовать VBScript.

Я использовал это для моего эмулятора GameBoy Color.

Вот скрипт PHP, который творит чудеса:

<?php
//Binary Proxy
if (isset($_GET['url'])) {
    try {
        $curl = curl_init();
        curl_setopt($curl, CURLOPT_URL, stripslashes($_GET['url']));
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($curl, CURLOPT_USERAGENT, $_SERVER['HTTP_USER_AGENT']);
        curl_setopt($curl, CURLOPT_POST, false);
        curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 30);
        $result = curl_exec($curl);
        curl_close($curl);
        if ($result !== false) {
            header('Content-Type: text/plain; charset=ASCII');
            header('Expires: '.gmdate('D, d M Y H:i:s \G\M\T', time() + (3600 * 24 * 7)));
            echo(base64_encode($result));
        }
        else {
            header('HTTP/1.0 404 File Not Found');
        }
    }
    catch (Exception $error) { }
}
?>
person Grant Galitz    schedule 29.09.2010