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

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

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

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

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

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

Код для конвертации предоставил Мискун.

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

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

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

var data = xhr.responseBody.toArray();

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

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

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

For comparison, the byte-by-byte conversion through loops takes several minutes for this same 500kB file, so it's a no-brainer:) Below the code I have been using, to insert into your header. Then call the function ieGetBytes with your 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]-->

Я пытался скачать файл, а затем подписать его, используя 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);
}

Вы также можете просто создать прокси-скрипт, который идет по адресу, который вы запрашиваете, и это 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) { }
}
?>

Большое спасибо за это решение. функция 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]);
    }

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

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

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

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.

Другие вопросы по тегам