44

我之前看到过很多与此类似的问题,但我还没有找到一个准确描述我当前问题的问题,所以这里是:

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

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

我已经研究了一段时间,发现如果Content-Length没有与请求的 AJAX 资源一起发送正确的标头,则onProgress事件处理程序无法按预期运行,因为它不知道下载的进度。发生这种情况时,将在事件对象上lengthComputable设置一个名为的属性。false

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

所以我的问题是:有没有办法使用 AJAX Progress API 压缩/压缩内容?如果是这样,我现在做错了什么?


这是资源在 Chrome 网络面板中的显示方式,表明压缩正在工作:

网络面板

这些是相关的请求标头,表明请求是 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 处理。


根据亚当的建议,我尝试将服务器端切换为 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 的请求,请求如下:

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

8 回答 8

16

您的解决方案的一个稍微更优雅的变体是在您的 HTTP 响应中设置一个像“x-decompressed-content-length”之类的标头,并使用内容的完整解压缩值(以字节为单位),并从 onProgress 中的 xhr 对象中读取它处理程序。

您的代码可能类似于:

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);
};
于 2015-09-26T17:06:06.217 回答
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-LengthGETHEAD请求头(应该代表未压缩的内容长度),并中止发送HEAD请求中的内容。

在 PHP 中,设置标头如下所示:

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

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

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

这是它的实际效果:

截屏

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

于 2013-03-08T20:44:59.980 回答
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



相关链接:

于 2013-03-06T12:08:00.433 回答
2

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

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

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

维基百科上的 Gzip

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

为什么对 Apache 提供的文本文件使用 deflate 而不是 gzip?

在你的问题的情况下,浏览器可能会看到一个放气文件从管道中传下来,然后举起手臂说:“当我什至不知道我将如何最终解码这个东西时,你怎么能期望我担心进展是否正确,人类?”

如果您切换服务器配置以便对响应进行 gzip 压缩(即 gzip 显示为内容编码),我希望您的脚本能够像您希望/预期的那样工作。

于 2013-03-02T08:23:07.823 回答
2

我们创建了一个库来估计进度并始终设置lengthComputable为 true。

Chrome 64 仍然存在这个问题(参见Bug

这是一个 javascript shim,您可以将其包含在您的页面中,它可以解决此问题,并且您可以new XMLHTTPRequest()正常使用该标准。

可以在这里找到 javascript 库:

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

于 2018-03-06T14:35:37.733 回答
1

这个解决方案对我有用。

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

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

第一步是检查多个文件之间压缩的一般差异,并查看是否乘以 2 例如结果与解压缩文件大小(即原始大小)最接近的结果并相应地相乘,但通过相乘确保结果仍然更小或相等但不大于原始文件大小,因此对于加载的数据,它保证达到并且很可能在所有情况下都略超过 100。此外,此问题解决方案还有一个 hacky 增强功能,即通过将进度计算限制为 100,并且无需检查进度是否超过,同时必须解决确保达到 100% 实施的相关点。原始大小并相应地相乘,但通过相乘确保结果仍然小于或等于但不大于原始文件大小,因此对于加载的数据,它保证在所有情况下都达到并且最有可能略超过 100。此外,此问题解决方案还有一个 hacky 增强功能,即通过将进度计算限制为 100,并且无需检查进度是否超过,同时必须解决确保达到 100% 实施的相关点。原始大小并相应地相乘,但通过相乘确保结果仍然小于或等于但不大于原始文件大小,因此对于加载的数据,它保证在所有情况下都达到并且最有可能略超过 100。此外,此问题解决方案还有一个 hacky 增强功能,即通过将进度计算限制为 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 第一个请求的服务器中的感兴趣文件的大小列表,然后在第二个请求中,预加载请求将具有每个相关的原始文件大小,甚至在作为代码的一部分手动存储之前,预加载的资源在关联数组中固定解压缩大小,然后可以使用它来跟踪加载进度。

对于我的跟踪加载进度实施实例,请参阅我的个人网站https://zakaria.website中的资源预加载。

最后,我不知道增加 deflate 缓冲区大小的任何不利因素,除了服务器内存的额外负载,如果有人对此问题有意见,我们将非常感激让我们知道。

于 2020-11-21T14:09:09.947 回答
0

我能想到的唯一解决方案是手动压缩数据(而不是将其留给服务器和浏览器),因为这允许您使用正常的进度条,并且仍然应该比未压缩版本给您带来可观的收益。例如,如果系统只需要在最新一代的 Web 浏览器中工作,您可以在服务器端压缩它(无论您使用什么语言,我相信有一个 zip 函数或库),在客户端您可以使用压缩包。如果需要更多浏览器支持,您可以查看此 SO 答案对于许多压缩和解压缩功能(只需选择一个您正在使用的服务器端语言支持的功能)。总体而言,这应该相当容易实现,尽管它的性能会比原生压缩/解压缩更差(尽管可能仍然很好)。(顺便说一句,如果您选择适合您正在使用的数据类型并且数据足够大的压缩算法,在理论上它可以比本机版本执行得更好)

另一种选择是使用 websocket 并在加载的同时解析/处理每个部分的部分中加载数据(您不需要 websockets,但是在彼此之后执行 10 个 http 请求可能会很麻烦)。这是否可能取决于具体场景,但在我看来,报告数据是可以部分加载的数据,不需要先完全下载。

于 2013-03-06T13:39:49.157 回答
-2

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

您可能会尝试远离 jQuery 或 hack 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 函数不支持进度事件处理程序,或者它不是 jQuery 文档的一部分(请参阅下面的评论)。

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

*更新3 *

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

于 2013-03-08T00:11:57.487 回答