如何在XHR onProgress函数中使用缩减/ gzipped内容?

时间:2013-02-26 19:34:35

标签: javascript jquery xmlhttprequest compression

我之前看到过一些类似的问题,但我还没有找到一个完全描述我当前问题的问题,所以这里有:

我有一个页面,它通过AJAX加载一个大的(0.5到10 MB之间)JSON文档,以便客户端代码可以处理它。加载文件后,我没有任何我不期望的问题。但是,下载需要很长时间,因此我尝试利用XHR Progress API呈现进度条以向用户指示文档正在加载。这很有效。

然后,为了加快速度,我尝试通过gzip和deflate压缩服务器端的输出。这也很有效,但是,我的进度条停止了工作。

我已经查看了一段时间的问题并发现如果没有使用请求的AJAX资源发送正确的Content-Length标头,onProgress事件处理程序无法按预期运行,因为它不会我不知道下载有多远。发生这种情况时,名为lengthComputable的属性在事件对象上设置为false

这是有道理的,所以我尝试使用未压缩和压缩的输出长度显式设置标头。我可以验证是否正在发送标头,我可以验证我的浏览器知道如何解压缩内容。但onProgress处理程序仍会报告lengthComputable = false

所以我的问题是:有没有办法使用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-LengthContent-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)连接上尝试了这一点,没有区别:内容在浏览器中正常加载,但不由Progress API处理。 / p>


Per Adam's suggestion,我尝试将服务器端切换到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。


Bertrand's request,这是请求:

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

8 个答案:

答案 0 :(得分:11)

您的解决方案稍微更优雅的变体是设置一个标题,如'x-decompressed-content-length'或HTTP响应中的任何内容,以及以字节为单位的内容的完全解压缩值,并从xhr对象中读取它在你的onProgress处理程序中。

您的代码可能类似于:

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

答案 1 :(得分:9)

我无法解决在压缩内容本身上使用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-LengthGET请求(应代表未压缩的内容长度)上设置HEAD标头,并且中止在HEAD请求上发送内容。

在PHP中,设置标题如下:

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

如果内容是HEAD请求,则中止发送内容:

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

以下是行动中的样子:

screenshot

HEAD在下面的屏幕截图中花了这么长时间的原因是因为服务器仍然需要解析文件才能知道它有多长,但这是我可以肯定改进的东西,而且& #39;绝对是改进的地方。

答案 2 :(得分:4)

不要因为没有原生解决方案而陷入困境;一行黑客可以解决你的问题,而不会搞乱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 MB



相关链接:

答案 3 :(得分:2)

尝试将服务器编码更改为gzip。

您的请求标头显示三种可能的编码(gzip,deflate,sdch),因此服务器可以选择这三种中的任何一种。通过响应标题,我们可以看到您的服务器选择使用deflate进行响应。

Gzip是一种编码格式,除了额外的页眉和页脚(包括原始的未压缩长度)和不同的校验和算法外,还包括deflate有效负载:

Gzip at Wikipedia

Deflate存在一些问题。由于遗留问题涉及不正确的解码算法,deflate的客户端实现必须通过愚蠢的检查才能确定他们正在处理哪些实现,不幸的是,他们经常仍然弄错:

Why use deflate instead of gzip for text files served by Apache?

就你的问题而言,浏览器可能会看到管道中出现一个deflate文件并且只是耸了耸肩说:“当我甚至不知道我将如何最终解码这个东西时,如何你能指望我担心人的进步是正确的吗?“

如果您切换服务器配置以使响应被gzip压缩(即gzip显示为内容编码),我希望您的脚本按照您希望/期望的方式工作。

答案 4 :(得分:2)

我们创建了一个估算进度的库,并始终将lengthComputable设置为true。

Chrome 64仍然存在此问题(请参阅Bug

这是一个javascript填充程序,您可以在页面中包含修复此问题的内容,并且您可以正常使用标准new XMLHTTPRequest()

可在此处找到javascript库:

https://github.com/AirConsole/xmlhttprequest-length-computable

答案 5 :(得分:1)

此解决方案对我有用。

我增加了deflate缓冲区的大小以覆盖我可能拥有的最大文件大小,该文件通常将被压缩到10mb左右,并且在apache配置中它从9.3mb压缩到3.2mb,因此content-length标头应为返回而不是省略,因为在加载压缩文件时使用的传输编码规范超出了缓冲区大小,有关压缩中使用的分块编码头的更多信息以及有关缩小缓冲区大小的更多信息,请参考https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Transfer-Encodinghttps://httpd.apache.org/docs/2.4/mod/mod_deflate.html#deflatebuffersize中。

1-在您的apache配置中包括以下内容,并且注释缓冲区大小值以字节为单位。

<IfModule mod_deflate.c>
DeflateBufferSize 10000000
</IfModule>

2-重新启动apache服务器。

3-在您的.htaccess文件中添加以下内容,以确保content-length标头公开给JS HTTP请求。

<IfModule mod_headers.c>
    Header set Access-Control-Expose-Headers "Content-Length"
</IfModule>

4-在计算进度总计百分比之前的onDownloadProgress事件中,追加以下内容以获取总字节值。

var total = e.total;
if(!e.lengthComputable){
total = e.target.getResponseHeader('content-length') * 2.2;
} 

5-请注意,通过比较我了解到,lengthComputable设置为false,因为该标志指示是否在标头中传递了content-length,而我并不依赖于Content-Length标头的遗漏,而是实际上是Content-Encoding标头在文件响应标头中传递时发现lengthComputable,然后将其设置为false,这是JS HTTP请求规范中一部分的正常行为。此外,我之所以将压缩内容长度的总和乘以2.2,是因为通过我的服务器压缩级别和方法实现更准确的下载/上传进度跟踪,因为返回的HTTP进度中的已加载总数反映了已解压缩的数据总数,而不是已压缩的数据,因此需要稍微调整代码逻辑来满足您的服务器压缩方法,它可能与我的有所不同,第一步是检查多个文件之间压缩的一般差异,并查看是否乘以2,例如结果与解压缩的文件大小(即原始大小)最接近的值,并相应地相乘,但还要确保通过相乘得出的结果仍小于或等于但不大于原始文件大小,因此对于已加载的数据,其保证的到达范围以及最有可能的在所有情况下都略高于100。另外,此问题解决方案还有一个很强的增强功能,即将进度计算的上限设置为100,因此无需检查进度是否超出了限制,而必须确保确保实现100%的相关点得到解决。

在我的情况下,这使我可以知道何时每个文件/资源​​加载已完成,即检查总计,如下所示:> =过去曾考虑将压缩后的总乘法数略微超过100%以达到解压缩,或者百分比计算方法上限为100,然后使用==运算符代替,以查找每个文件何时完成预加载。此外,我想通过从每个文件中存储固定的解压缩后的加载总数(即原始文件大小)并在预加载文件期间使用它来从根本上解决此问题。例如根据我的条件下的资源来计算进度百分比。这是我的onProgress事件处理条件中的以下代码片段。

// Some times 100 reached in the progress event more than once.
if(preloadedResources < resourcesLength && progressPercentage < 100) {
    canIncreaseCounter = true;
}
if(progressPercentage >= 100 && canIncreaseCounter && preloadedResources < resourcesLength) {
    preloadedResources++;
    canIncreaseCounter = false;
}

另外,请注意预期的加载总使用量是固定的解决方案,在所有情况下都有效,除非自己无法事先访问要预加载或下载的文件,而且我认为这种情况很少发生,因为大多数时候我们知道我们想要的文件要进行预加载,因此可以通过在PHP脚本中使用HTTP优先级请求位于服务器中的目标文件的大小的PHP脚本提供服务,从而可以在预加载之前获取其大小,然后在第二次预加载请求中将具有每个相关的原始文件大小甚至在手动存储为代码的一部分之前,将预加载的资源解压缩后的大小固定在关联数组中,然后可以使用它来跟踪加载进度。

有关我的跟踪加载进度实施实时示例,请参见我的个人网站https://zakaria.website中的资源预加载。

最后,除了服务器内存的额外负载外,我不知道放气缓冲区大小会增加的任何弊端,并且如果有人对此问题有所投入,请告诉我们。

答案 6 :(得分:0)

我能想到的唯一解决方案是手动压缩数据(而不是将其留给服务器和浏览器),因为这样可以使用正常的进度条,并且仍然可以比未压缩的版本获得相当大的收益。例如,如果系统只需要在最新一代Web浏览器中工作,您可以在服务器端拉链(无论您使用何种语言,我确信有拉链功能或库),在客户端,您可以使用zip.js。如果需要更多浏览器支持,您可以检查this SO answer以获取许多压缩和解压缩功能(只需选择您正在使用的服务器端语言支持的功能)。总的来说,这应该是相当简单的实现,虽然它会比本机压缩/解压缩表现更差(尽管可能仍然很好)。 (顺便说一句,在给它多一点之后认为理论上它可以比原生版本更好,以防你选择适合你正在使用的数据类型并且数据足够大的压缩算法)

另一个选择是使用websocket并将数据加载到您在加载的同时解析/处理每个部分的部分(您不需要websockets,但是在另外一个部分之后可以执行10个http请求)相当麻烦)。这是否可能取决于具体情况,但对我而言,报告数据听起来像是可以部分加载的数据类型,不需要首先完全下载。

答案 7 :(得分:-2)

我不清楚这个问题,因为解压缩应该由浏览器完成,所以不应该发生。

你可能会试图摆脱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

您可以尝试通过javascript解压缩json的内容(请参阅注释中的资源)。

*更新2 *

$ .ajax函数不支持progress事件处理程序,或者它不是jQuery文档的一部分(请参阅下面的注释)。

这是一种让这个处理程序工作的方法,但我自己从未尝试过: http://www.dave-bond.com/blog/2010/01/JQuery-ajax-progress-HMTL5/

*更新3 *

解决方案使用tierce第三方库扩展(?)jQuery ajax功能,所以我的建议不适用