【问题标题】:How can I use deflated/gzipped content with an XHR onProgress function?如何通过 XHR onProgress 函数使用压缩/压缩内容?
【发布时间】:2013-02-12 09:52:19
【问题描述】:

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

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

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

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

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

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


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

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

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


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

【问题讨论】:

  • 作为 AJAX 基础的意识形态之一是能够按需延迟加载数据片段。为什么不根据需要使用 ajax 而不是整个堆加载这些数据的一部分?
  • @Kristian 没有过多的细节,我有点需要整个事情。我使用 AJAX 的唯一原因(而不仅仅是将它与主请求一起放入)是因为我想快速将某些内容放在屏幕上,以便用户知道正在发生的事情。
  • 这个 mozilla 错误看起来很有趣:bugzilla.mozilla.org/show_bug.cgi?id=614352
  • @JimmySawczuk 我完全理解。以我的经验,通常有一条更简单的道路,而不是为了你正在做的事情而兼顾标题和压缩。但既然我相信你明白你要做什么,祝你好运,伙计!
  • 我认为问题是如果你压缩它,会有一个 transfer-encoding ,它可能禁止使用 content-length 并且 onProgress 正在尝试测量未压缩的长度。它不知道最终未压缩内容的大小,因此无法工作。传输可能以分块模式完成,传输结束时传输结束。

标签: javascript jquery xmlhttprequest compression


【解决方案1】:

您的解决方案的一个稍微更优雅的变体是在您的 HTTP 响应中设置一个像“x-decompressed-content-length”这样的标头或任何内容,以字节为单位的内容的完整解压缩值,然后从 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);
};

【讨论】:

  • 这是本页所有其他解决方案的最佳解决方案。
  • 这个解决方案效果很好,见here
  • 当您从响应标头中获取contentLength 时,您不应该调用parseInt 吗?
  • @JohnWeisz 可能应该。尽管它在 JavaScript 中的 / 运算符执行类型转换时仍然有效。我会解决的。
  • 我看到e.loaded 显示了在 Firefox 中下载的压缩字节数,以及在 Chrome 中下载的未压缩字节数。但是,此解决方案仍然可以正常工作,因为 e.lengthComputable 在 Firefox 中是 true 而在 Chrome 中是 false
【解决方案2】:

我无法解决在压缩内容本身上使用onProgress 的问题,但我想出了这个半简单的解决方法。 简而言之:在发送GET 请求的同时向服务器发送HEAD 请求,并在有足够信息时呈现进度条。


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>', {});

在服务器端,在GETHEAD 请求上设置X-Content-Length 标头(应该代表未压缩 内容长度),并中止发送内容到HEAD 请求。

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

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

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

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

下面是实际效果:

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

【讨论】:

  • 现在您有 2 个 HTTP 请求需要成功。不太可能,但从统计上讲,它确实增加了页面损坏的可能性。另外,您有一个自定义的 HTTP 标头,可能会使来这里复制/粘贴代码以解决相同问题的人感到困惑。如果你想解耦,最好使用 GET 参数,你可以用 JS 提取:download.html?file=data.json&amp;size=6666
  • @IvanCastellanos 如果HEAD 请求失败,这没什么大不了的,如果GET 请求失败,我也不会比以前更好。我理论上可以设置正常的Content-Length 标头,但它可能会错误地代表HEAD 请求中实际发送的内容,因此自定义的更合适。 (我可能会澄清我的答案的那部分。)我也没有遵循您的 GET 参数如何解决我的问题。
  • 下载页面的链接将始终包含“内容长度”的信息,使其更便携的解决方案(不需要 php 脚本加上一个更少的 HTTP 请求)。 content_length=document.location.href.match(/(?:size=)(\d+)/)[1]|0;
  • 这并不能解决我的问题,因为我不知道在页面加载用户需要什么文件或它们会有多大。
  • 我明白了。也许使用 PHP 更新页面本身? if (!isset($_GET['size'])) header("Location: ". $_SERVER["REQUEST_URI"]."&amp;size=".filesize($_GET["file"])) 之类的东西,但可能是因为我喜欢在需要最大解耦时做类似的事情。
【解决方案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 MB



相关链接:

【讨论】:

  • 整个问题是,在 javascript 中,文件大小没有被报告,因为它只能在加载整个文档后计算。知道原始文件的大小对此无济于事。
  • 什么?阅读 client.responseText.length 就像您在我的示例中看到的那样,您知道已经加载的确切字节数;我刚刚用 Apache 和一个在 gzip 中传输的大 .json 文件(使用 mod_deflate 模块)对其进行了测试。
  • 由于我无法在这台计算机上轻松测试它,在这种情况下您能否告诉我您的 client.responseText 字符串中包含什么?根据我的经验,在加载整个文件之前,字符串应该为空(如果文件被压缩)。如果有任何evt.loaded 可能有效,但我必须检查一下。此外,在这种情况下,您需要使用压缩文件的文件大小,而不是原始文件。哦,你是不是对我的回答投了反对票?
  • 这也不是一个坏主意,但我希望有一个更清洁的解决方案。
  • 除了解决你的问题,你还需要别的东西,好的。您还可以使用 .getResponseHeaders() 并使用一点正则表达式;也许这对你来说已经足够干净了,但我不确定了。
【解决方案4】:

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

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

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

Gzip at Wikipedia

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

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

就你的问题而言,浏览器可能会看到一个放气文件从管道中传来,然后举起手臂说:“当我什至不知道我将如何最终解码这个东西时,如何你能指望我担心进展顺利吗,人类?”

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

【讨论】:

  • 嘿@Adam,感谢您的回复。我尝试切换到 gzip,但没有成功。我将响应标题放在上面的问题中。 :-\
  • AFAIK 这仅适用于静态 gzip,对吧?
【解决方案5】:

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

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

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

javascript 库可以在这里找到:

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

【讨论】:

    【解决方案6】:

    这个解决方案对我有用。

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

    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。此外,此问题解决方案还有一个 hacky 增强功能,即通过将进度计算限制为 100,并且无需检查进度是否超过,同时必须解决确保达到 100% 实施的相关点。

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

    // 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 缓冲区大小的任何缺点,除了服务器内存的额外负载,如果有人对此问题有意见,非常感谢让我们知道。

    【讨论】:

      【解决方案7】:

      我能想到的唯一解决方案是手动压缩数据(而不是将其留给服务器和浏览器),因为这样可以让您使用正常的进度条,并且与未压缩的版本相比,它仍然会给您带来可观的收益。例如,如果系统只需要在最新一代的网络浏览器中工作,您可以在服务器端将其压缩(无论您使用什么语言,我相信都有一个 zip 函数或库),在客户端您可以使用zip.js。如果需要更多浏览器支持,您可以查看this SO answer 以了解一些压缩和解压缩功能(只需选择您使用的服务器端语言支持的一种)。总体而言,这应该相当容易实现,尽管它的性能会比原生压缩/解压缩更差(尽管可能仍然很好)。 (顺便说一句,经过深思熟虑后,理论上它的性能甚至可以比原生版本更好,以防您选择适合您正在使用的数据类型且数据足够大的压缩算法)

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

      【讨论】:

      • 我曾考虑过这个解决方案,但真的希望避免它。感谢您提供参考链接。
      • 如果evt.loaded 真的拥有一个有效值,那么使用实际的内容长度当然会更好(这之前被窃​​听,但xhr.getResponseHeader("Content-Length") 现在可能工作),但从你的回答我假设它不适用于压缩数据。 (虽然现在我很困惑,因为我不再确定内容长度是压缩内容的长度还是未压缩内容的长度:S 我已经有一段时间没有用这个做了一些东西了......)
      • 哦,顺便说一句,您总是可以自己添加另一个标头,您可以从 javascript 中读取该标头,确保所有内容都在一个请求中(仍然提供 evt.loaded 有效)。
      • content-length 通常是压缩文件的大小。但由于 Apache 使用渐进式编码,我不建议使用它。 (默认情况下,您事先不知道最终尺寸)
      【解决方案8】:

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

      您可能会尝试远离 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 内容(请参阅 cmets 中的资源)。

      * 更新 2 *

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

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

      * 更新 3 *

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

      【讨论】:

      • 解压由浏览器完成的,它计算内容长度,这样我就可以知道我下载了多少,我正在努力解决。
      • 好的,那么您能否在 firebug 中关注 evt,例如,在 onProgress 事件处理程序的函数(jqXHR,evt)中使用 console.log(evt)。我只是想帮忙。
      • 可以私信链接吗?
      • 进度在 jQuery 文档中不存在作为跟踪进度的处理程序
      猜你喜欢
      • 2011-02-26
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多