Как использовать дефлированный /gzipped контент с помощью функции XHR onProgress?

Я видел кучу подобных вопросов, о которых вы спрашивали раньше, но я не нашел тот, который точно описывает мою текущую проблему, поэтому:

У меня есть страница, которая загружает большой (от 0,5 до 10 МБ) JSON-документ через AJAX, чтобы клиентский код мог его обработать. Как только файл загружен, у меня нет никаких проблем, которые я не ожидаю. Однако загрузка занимает много времени, поэтому я попытался использовать XHR Progress API, чтобы отобразить индикатор выполнения, чтобы указать пользователю, что Документ загружается. Это хорошо работает.

Затем, чтобы ускорить работу, я попытался сжать вывод на серверной стороне через gzip и сфотографировать. Это тоже сработало, но с огромным успехом мой индикатор прогресса перестает работать.

Я некоторое время изучал эту проблему и обнаружил, что если надлежащий заголовок Content-Length не отправляется с запрошенным ресурсом AJAX, обработчик события onProgress не может функционировать должным образом, поскольку он не знает, как далеко в процессе загрузки. Когда это произойдет, свойство lengthComputable устанавливается на false объекта события.

Это имело смысл, поэтому я попытался настроить заголовок явно как с несжатой, так и с сжатой длиной вывода. Я могу проверить, что отправляется заголовок, и я могу проверить, что мой браузер знает, как распаковывать содержимое. Но обработчик onProgress все еще сообщает lengthComputable = false.

Итак, мой вопрос: есть способ gzipped/deflated content с API AJAX Progress API? И если да, то что я делаю неправильно прямо сейчас?


Так отображается ресурс на панели "Сеть Chrome", показывая, что сжатие работает:

network panel

Это соответствующие заголовки запрос, показывающие, что запрос AJAX и что Accept-Encoding установлен правильно:

GET /dashboard/reports/ajax/load HTTP/1.1
Connection: keep-alive
Cache-Control: no-cache
Pragma: no-cache
Accept: application/json, text/javascript, */*; q=0.01
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_5) AppleWebKit/537.22 (KHTML, like Gecko) Chrome/25.0.1364.99 Safari/537.22
Accept-Encoding: gzip,deflate,sdch
Accept-Language: en-US,en;q=0.8
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3

Это соответствующие заголовки ответ, показывающие, что параметры Content-Length и Content-Type установлены правильно:

HTTP/1.1 200 OK
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Content-Encoding: deflate
Content-Type: application/json
Date: Tue, 26 Feb 2013 18:59:07 GMT
Expires: Thu, 19 Nov 1981 08:52:00 GMT
P3P: CP="CAO PSA OUR"
Pragma: no-cache
Server: Apache/2.2.8 (Unix) mod_ssl/2.2.8 OpenSSL/0.9.8g PHP/5.4.7
X-Powered-By: PHP/5.4.7
Content-Length: 223879
Connection: keep-alive

Для чего это стоит, я пробовал это как на стандартном (http), так и на защищенном (https) соединении без каких-либо различий: контент загружается в браузере, но не обрабатывается API-интерфейсом Progress.


Per предложение Adam, я попытался переключить серверную сторону на кодировку gzip без каких-либо успехов или изменений. Вот соответствующие заголовки ответов:

HTTP/1.1 200 OK
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Content-Encoding: gzip
Content-Type: application/json
Date: Mon, 04 Mar 2013 22:33:19 GMT
Expires: Thu, 19 Nov 1981 08:52:00 GMT
P3P: CP="CAO PSA OUR"
Pragma: no-cache
Server: Apache/2.2.8 (Unix) mod_ssl/2.2.8 OpenSSL/0.9.8g PHP/5.4.7
X-Powered-By: PHP/5.4.7
Content-Length: 28250
Connection: keep-alive

Просто повторить: содержимое загружается и декодируется должным образом, это просто API прогресса, с которым у меня возникают проблемы.


Per запрос Бертран, здесь запрос:

$.ajax({
    url: '<url snipped>',
    data: {},
    success: onDone,
    dataType: 'json',
    cache: true,
    progress: onProgress || function(){}
});

И вот обработчик событий onProgress, который я использую (это не слишком сумасшедший):

function(jqXHR, evt)
{
    // yes, I know this generates Infinity sometimes
    var pct = 100 * evt.position / evt.total;

    // just a method that updates some styles and javascript
    updateProgress(pct);
});

Ответ 1

Мне не удалось решить проблему использования onProgress для самого сжатого содержимого, но я придумал это полупростую обходную проблему. В двух словах: отправьте запрос HEAD на сервер в то же время, что и запрос GET, и отрисуйте индикатор выполнения, как только будет достаточно информации для этого.


function loader(onDone, onProgress, url, data)
{
    // onDone = event handler to run on successful download
    // onProgress = event handler to run during a download
    // url = url to load
    // data = extra parameters to be sent with the AJAX request
    var content_length = null;

    self.meta_xhr = $.ajax({
        url: url,
        data: data,
        dataType: 'json',
        type: 'HEAD',
        success: function(data, status, jqXHR)
        {
            content_length = jqXHR.getResponseHeader("X-Content-Length");
        }
    });

    self.xhr = $.ajax({
        url: url,
        data: data,
        success: onDone,
        dataType: 'json',
        progress: function(jqXHR, evt)
        {
            var pct = 0;
            if (evt.lengthComputable)
            {
                pct = 100 * evt.position / evt.total;
            }
            else if (self.content_length != null)
            {
                pct = 100 * evt.position / self.content_length;
            }

            onProgress(pct);
        }
    });
}

И затем использовать его:

loader(function(response)
{
    console.log("Content loaded! do stuff now.");
},
function(pct)
{
    console.log("The content is " + pct + "% loaded.");
},
'<url here>', {});

На стороне сервера установите заголовок X-Content-Length как для запросов GET, так и для HEAD (которые должны представлять длину несжатого контента) и прервать отправку содержимого по запросу HEAD.

В PHP установка заголовка выглядит так:

header("X-Content-Length: ".strlen($payload));

И затем отмените отправку содержимого, если оно HEAD:

if ($_SERVER['REQUEST_METHOD'] == "HEAD")
{
    exit;
}

Вот как это выглядит в действии:

screenshot

Причина, по которой HEAD занимает так много времени в приведенном ниже скриншоте, заключается в том, что сервер все еще должен разбирать файл, чтобы знать, как долго это происходит, но что-то, что я могу определенно улучшить, и это определенно улучшение от того, где это был.

Ответ 2

Несколько более элегантным вариантом для вашего решения было бы установить заголовок, такой как "x-decpressed-content-length" или что-то еще в вашем ответе HTTP с полным распакованным значением содержимого в байтах и ​​прочитать его с объекта xhr в обработчике onProgress.

Ваш код может выглядеть примерно так:

request.onProgress = function (e) {
  var contentLength;
  if (e.lengthComputable) {
    contentLength = e.total;
  } else {
    contentLength = e.target.getResponseHeader('x-decompressed-content-length');
  }
  progressIndicator.update(e.loaded / contentLength);
};

Ответ 3

Не застревайте только потому, что нет собственного решения; взлома одной строки может решить вашу проблему, не испортив конфигурацию Apache (что в некоторых хостах запрещено или очень ограничено):

PHP на помощь:

var size = <?php echo filesize('file.json') ?>;

Что это, вы, наверное, уже знаете все остальное, но как ссылка здесь:

<script>
var progressBar = document.getElementById("p"),
    client = new XMLHttpRequest(),
    size = <?php echo filesize('file.json') ?>;

progressBar.max = size;

client.open("GET", "file.json")

function loadHandler () {
  var loaded = client.responseText.length;
  progressBar.value = loaded;
}

client.onprogress = loadHandler;

client.onloadend = function(pe) {
  loadHandler();
  console.log("Success, loaded: " + client.responseText.length + " of " + size)
}
client.send()
</script>

Живой пример:

Другой пользователь SO считает, что я ложь относительно действительности этого решения, поэтому здесь он жив: http://nyudvik.com/zip/, это gzip- ed и реальные весы файлов 8 МБ



Ссылки по теме:

Ответ 4

Попробуйте изменить кодировку сервера на gzip.

В заголовке запроса отображаются три возможных кодировки (gzip, deflate, sdch), поэтому сервер может выбрать любой из этих трех. По заголовку ответа мы видим, что ваш сервер выбирает ответ с дефлятом.

Gzip - это формат кодирования, который включает в себя полезную нагрузку дефляции в дополнение к дополнительным верхним и нижним колонтитулам (включая исходную несжатую длину) и другой алгоритм контрольной суммы:

Gzip в Википедии

У Deflate есть некоторые проблемы. Из-за устаревших проблем, связанных с неправильными алгоритмами декодирования, клиентские реализации deflate должны проходить через глупые проверки, чтобы выяснить, с какой реализацией они работают, и, к сожалению, они часто все еще ошибаются:

Зачем использовать deflate вместо gzip для текстовых файлов, обслуживаемых Apache?

В случае вашего вопроса браузер, вероятно, видит файл дефляции, спускающийся по трубе, и просто подбрасывает руки и говорит: "Когда я даже не знаю точно, как я закончу расшифровку этой вещи, как можете ли вы ожидать, что я буду беспокоиться о том, чтобы добиться успеха, человек?"

Если вы переключите конфигурацию своего сервера, чтобы ответ был gzipped (т.е. gzip отображается как кодировка содержимого), я надеюсь, что ваш script работает так, как вы надеялись/ожидали.

Ответ 5

Единственное решение, о котором я могу думать, - это сжатие данных вручную (вместо того, чтобы оставлять его на сервере и в браузере), поскольку это позволяет вам использовать нормальный индикатор выполнения и все равно приносить вам значительный выигрыш по несжатой версии. Если, например, система требуется только для работы в веб-браузерах последнего поколения, вы можете, например, закрепить ее на стороне сервера (любой язык, который вы используете, я уверен, что есть zip-функция или библиотека), а на стороне клиента вы можете использовать zip.js. Если требуется дополнительная поддержка браузера, вы можете проверить этот ответ SO для ряда функций сжатия и декомпрессии (просто выберите тот, который поддерживается на используемом вами языке на стороне сервера). В целом это должно быть достаточно простым в реализации, хотя оно будет работать хуже (хотя все еще хорошо), чем собственное сжатие/декомпрессия. (Кстати, после того, как он немного подумал, что теоретически может работать даже лучше, чем родная версия, если вы выберете алгоритм сжатия, который будет соответствовать типу данных, которые вы используете, и данные достаточно большие)

Другим вариантом будет использование веб-раскладки и загрузка данных в тех частях, где вы будете разбирать/обрабатывать каждую часть в то же время, когда она загружается (для этого вам не нужны веб-сайты, но выполнение 10 http-запросов после каждого из них может быть довольно хлопот). Возможно ли это, зависит от конкретного сценария, но для меня это звучит так, как данные отчета - это вид данных, которые могут быть загружены по частям и не требуются для полной загрузки.

Ответ 6

Я не понимаю эту проблему, это не должно происходить, так как декомпрессия должна выполняться браузером.

Вы можете попытаться отойти от jQuery или взломать jQuery, потому что $.ajax не работает хорошо с двоичными данными:

Ссылка: http://blog.vjeux.com/2011/javascript/jquery-binary-ajax.html

Вы можете попытаться выполнить собственную реализацию запроса ajax См.: https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest/Using_XMLHttpRequest#Handling_binary_data

Вы можете попытаться распаковать json содержимое javascript (см. ресурсы в комментариях).

* ОБНОВЛЕНИЕ 2 *

функция $.ajax не поддерживает обработчик события прогресса или не является частью документации jQuery (см. комментарий ниже).

вот способ получить этот обработчик, но я никогда не пробовал его сам: http://www.dave-bond.com/blog/2010/01/JQuery-ajax-progress-HMTL5/

* ОБНОВЛЕНИЕ 3 *

Решение использует tierce стороннюю библиотеку для расширения (?) jQuery ajax functionnality, поэтому мое предложение не применяется