jQuery如何逐步读取AJAX流?

82

我读过这个问题,但这并没有完全回答我的问题。

不幸的是,自从上次接触AJAX以来,在XHR对象中发生了变化,因此在响应内容被完全填充之前,无法直接访问responseText

我需要编写一个页面,使用AJAX(最好是jQuery,但我也可以接受其他建议)从一个我无法控制的服务器通过HTTP检索CSV数据。响应数据可能非常大,一个兆字节的文本并不罕见。

服务器支持流式传输。是否仍然有办法直接从JavaScript获取正在返回的数据流?

我有一些选择:编写一些PHP代码,放在中间并使用某种“Comet”技术(长轮询、EventSource等),但如果可能的话,我宁愿避免这样做。

如果有关系,请假设用户拥有最新版本的Firefox / Chrome / Opera浏览器,旧版本浏览器的兼容性不是问题。


我知道这个问题已经有答案了,我之前做过类似的东西,看一下这个链接,如果必要的话可以借鉴一下。 http://jsfiddle.net/JmZCE/1/ - MrJD
6个回答

94

在输出文本或HTML时,这相当直接。以下是一个例子。

(但如果尝试输出JSON,则会遇到问题,我将在下面进一步解决。)

PHP文件

header('Content-type: text/html; charset=utf-8');
function output($val)
{
    echo $val;
    flush();
    ob_flush();
    usleep(500000);
}
output('Begin... (counting to 10)');
for( $i = 0 ; $i < 10 ; $i++ )
{
    output($i+1);
}
output('End...');

HTML文件

<!DOCTYPE>
<html>
    <head>
        <title>Flushed ajax test</title>
        <meta charset="UTF-8" />
        <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script>
    </head>
    <body>
        <script type="text/javascript">
        var last_response_len = false;
        $.ajax('./flushed-ajax.php', {
            xhrFields: {
                onprogress: function(e)
                {
                    var this_response, response = e.currentTarget.response;
                    if(last_response_len === false)
                    {
                        this_response = response;
                        last_response_len = response.length;
                    }
                    else
                    {
                        this_response = response.substring(last_response_len);
                        last_response_len = response.length;
                    }
                    console.log(this_response);
                }
            }
        })
        .done(function(data)
        {
            console.log('Complete response = ' + data);
        })
        .fail(function(data)
        {
            console.log('Error: ', data);
        });
        console.log('Request Sent');
        </script>
    </body>
</html>

如果我需要用JSON怎么办?

实际上不可能在单个JSON对象完全加载之前增量地加载它(因为在你拥有完整的对象之前,语法始终无效)。

但是,如果您的响应具有多个连续的JSON对象,则可以随着它们逐个到达,逐个加载它们。

所以我通过以下方式修改了我的代码...

  1. 将PHP FILE第4行从echo $val;更改为echo '{"name":"'.$val.'"};'。 这将输出一系列JSON对象。

  2. 将HTML FILE第24行从console.log(this_response);更改为

    this_response = JSON.parse(this_response);
    console.log(this_response.name);
    
    请注意,这个简单的代码假设传输到浏览器的每个“块”都是有效的JSON对象。但这并不总是如此,因为你无法预测数据包的到达方式——你可能需要根据分号拆分字符串(或使用另一个分隔符)。不要将头文件更改为“application/json”,因为这会导致浏览器等待完全响应,并尝试解析响应是否为JSON,从而引发问题。可以在客户端禁用此检查。
    $.ajax(..., {dataType: "text"})
    

    希望一些人会觉得这很有用。


1
哇,谢谢您先生,这正是我在寻找的!非常好的示例,展示了如何使用JSON技术。 - Aaron
3
非常感谢,这只用了我1分钟就成功地实现了。真是太棒了。 - Pål Thingbø
1
使用 {dataType:"text"} 调用 $.ajax,这将禁止智能猜测(参见 http://api.jquery.com/jquery.ajax/ dataType)。 - Christophe Quintard
1
是的,您可以使用流式JSON解析器(例如oboe)逐步读取JSON(http://oboejs.com/)。您无需更改JSON响应以具有多个JSON对象,从设计角度来看,最好不要这样做。 - mwag
2
关于PHP的注意事项:通过连接字符串(例如echo '{"name":"'.$val.'"};')手动创建JSON通常是不好的实践。更好的代码可能是echo json_encode(["name"=>$val]).";"; - Laef
显示剩余3条评论

34

使用XMLHttpRequest.js

https://github.com/ilinsky/xmlhttprequest

http://code.google.com/p/xmlhttprequest

  • 提供了遵循标准(W3C)的、不会干扰页面的跨浏览器XMLHttpRequest 1.0对象实现
  • 修复所有浏览器在其原生XMLHttpRequest对象实现中观察到的怪癖
  • 使XMLHttpRequest对象活动记录透明化

要使用PHP进行长轮询(long polling):

output.php:

<?php
header('Content-type: application/octet-stream');

// Turn off output buffering
ini_set('output_buffering', 'off');
// Turn off PHP output compression
ini_set('zlib.output_compression', false);
// Implicitly flush the buffer(s)
ini_set('implicit_flush', true);
ob_implicit_flush(true);
// Clear, and turn off output buffering
while (ob_get_level() > 0) {
    // Get the curent level
    $level = ob_get_level();
    // End the buffering
    ob_end_clean();
    // If the current level has not changed, abort
    if (ob_get_level() == $level) break;
}
// Disable apache output buffering/compression
if (function_exists('apache_setenv')) {
    apache_setenv('no-gzip', '1');
    apache_setenv('dont-vary', '1');
}

// Count to 20, outputting each second
for ($i = 0;$i < 20; $i++) {
    echo $i.str_repeat(' ', 2048).PHP_EOL;
    flush();
    sleep(1);
}

run.php:

=>

run.php:

<script src="http://code.jquery.com/jquery-1.6.4.js"></script>
<script src="https://raw.github.com/ilinsky/xmlhttprequest/master/XMLHttpRequest.js"></script>

<script>
$(function() {
    var xhr = new XMLHttpRequest();
    xhr.open('GET', '/longpoll/', true);
    xhr.send(null);
    var timer;
    timer = window.setInterval(function() {
        if (xhr.readyState == XMLHttpRequest.DONE) {
            window.clearTimeout(timer);
            $('body').append('done <br />');
        }
        $('body').append('state: ' + xhr.readyState + '<br />');
        console.log(xhr.responseText);
        $('body').append('data: ' + xhr.responseText + '<br />');
    }, 1000);
});
</script>

这将输出:

state: 3
data: 0
state: 3
data: 0 1
state: 3
data: 0 1 2
state: 3
data: 0 1 2 3
state: 3
data: 0 1 2 3 4
...
...
...
state: 3
data: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
state: 3
data: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
state: 3
data: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
done
state: 4
data: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 

针对IE浏览器,您需要查看 XDomainRequest。

http://blogs.msdn.com/b/ieinternals/archive/2010/04/06/comet-streaming-in-internet-explorer-with-xmlhttprequest-and-xdomainrequest.aspx

http://msdn.microsoft.com/en-us/library/cc288060(VS.85).aspx


1
@Josh,是的,确实需要这样做。但使用长轮询时存在各种怪异问题。在读取状态发生更改之前,您需要发送2Kb的数据,并将内容类型设置为application/octet-stream。请参阅我更新的帖子,其中包含PHP示例。 - Petah
+1 显示所有妨碍去除缓冲输出的方法 :) - Peter Remmers
1
@xorinzor http://pastebin.com/3Dbt2mhQ 根据您的需求,您可能需要实现自定义协议。 这样可以读取所有数据直到“;”。 - Petah
为什么我需要使用echo $i.str_repeat(' ', 2048).PHP_EOL;而不是只使用echo $i? - Bakalash
3
由于一些浏览器在发送2KB的输出之前不会允许流式传输。 - Petah
显示剩余4条评论

21

在这种情况下,您需要使用纯JavaScript。原因是您需要持续轮询而不等待回调函数执行。您不需要jQuery,它非常简单。他们在Ajax Patterns网站上有一些不错的源代码

基本上,您只需跟踪响应中的最后位置,并定期轮询超过该位置的更多文本。在您的情况下,不同之处在于您可以订阅完成事件并停止轮询。


3
你能给我一个可行的例子吗?你提供的链接说,“XMLHttpRequest 的 responseText 属性始终包含已从服务器刷新出的内容,即使连接仍然打开。”根据我所读的资料,在较新版本的浏览器中不再是这种情况了。 - Josh
这只是在IE中吗?我认为readyState 3在其他浏览器中也包含它。 - scottheckel
1
主要是根据这个jQuery插件中的注释进行操作:http://plugins.jquery.com/project/ajax-http-stream “注意:我注意到这已经不再适用于Firefox 3.0.11(在Linux上的3.0.8可以使用),IE8或最新版本的Chrome。显然,趋势是在请求完成之前禁止访问xmlhttprequest.responseText(在我看来很愚蠢)。对此我很抱歉,我无能为力。” - Josh
事实证明,这确实可以使用纯JavaScript来完成,在尝试后(至少对于行为正常的浏览器)。仍然希望找到一个jQuery版本,以便在所有浏览器上正确运行,但目前这是最好的答案。 - Josh
死链让我感到难过。 - captncraig
该链接来自archive.org。有时您需要多次单击才能加载它们。这些文件必须追溯到很久以前。 - geekinit

16

既然你说你的服务器适合流式(异步)并正在寻找jQuery方案,那么你是否查看过jQuery Stream Plugin

它非常容易使用,让你不用担心太多。它也有相当好的文档


我可以看一下这个。快速浏览API页面,我没有看到发送HTTP POST和基本身份验证信息到服务器的方法,但我相信它一定在那里。也许“流友好”是一个错误的术语选择。我不是指异步或双向的。我的意思是它会像巨大的HTTP响应一样,在流中随时间发送大量数据。同时,我已经找到了一个非jQuery解决方案,应该对我的原始目的足够好。 - Josh
对于HTTP POST和基本身份验证,你可以直接使用jQuery。 - g19fanatic
我怎么样才能将“纯粹的jquery”与jquery流插件集成呢?文档在这一点上并不清楚。有示例吗? - Josh
5
现在它已经变成了一个门户网站,看起来非常棒,包括WebSockets等所有功能。 https://github.com/flowersinthesand/portal - marsbard
1
@marsbard门户已经到达了生命周期结束,不再维护!请使用Vibe - Donghwan Kim
显示剩余2条评论

2
我必须为一个网格提供大量的JSON数据,但它一直超过了允许的最大大小限制。我正在使用MVC和jquery,因此我采用了AlexMorley-Finch的解决方案。
服务器代码来自于"使用Web API进行流数据处理"。同时也可以参考https://github.com/DblV/StreamingWebApi
public class StreamingController : ApiController
{

    [HttpGet]
    [ActionName("GetGridDataStream")]
    public HttpResponseMessage GetGridDataStream(string id)
    {
        var response = Request.CreateResponse();
        DynamicData newData = new DynamicData();
        var res = newData.GetDataRows(id);
        response.Content = new PushStreamContent((stream, content, context) =>
        { 
            foreach (var record in res)
            {
                var serializer = new JsonSerializer();
                using (var writer = new StreamWriter(stream))
                {
                    serializer.Serialize(writer, record);
                    stream.Flush();
                }

               // Thread.Sleep(100);
            }

            stream.Close();
        });

        return response;
    }
}

这产生了一串需要用逗号进行分隔并用 [ ] 包围才能成功解析为 json 的 {json object}{json object}{json object} 流。
客户端代码因此提供了缺失的字符:
 var jsonData = {}; 

 $.ajax("api/Streaming/GetGridDataStream/" + viewName, {
    xhrFields: {
            onprogress: function (e) { 
                // console.log(this_response);
            }
        }
    }, { dataType: "text" }) //<== this is important for JSON data
    .done(function (data) { 

        data = "[" + data.replace(/\}\{/gi, "},{") + "]";

        jsonData["DataList"] = JSON.parse(data);
        //more code follows to create grid
    })
    .fail(function (data) {
        console.log('Error: ', data);
    });

我希望这能帮助那些使用 .Net MVC 和 jQuery 的人。

0

以下是使用 JQuery 实现此操作的简单方法(由 OP 要求):

首先,通过运行 https://gist.github.com/chrishow/3023092 中底部附加的代码来扩展 ajax 对象以支持 onreadystatechange。然后只需使用 onreadystatechange 函数调用 ajax,该函数将检查 xhr.responseText 是否有新文本。

如果您想变得更加花哨,可以每次读取 responseText 数据时清除它,如 此处 所述。

例如,请参见 https://jsfiddle.net/g1jmwcmw/1/,其将从 https://code.jquery.com/jquery-1.5.js 下载响应并将其分块输出到您的控制台窗口中,使用下面的代码(您只需将其复制到 html 页面中,然后在浏览器中打开即可):

<!-- jquery >= 1.5. maybe earlier too but not sure -->
<script src=https://code.jquery.com/jquery-1.5.min.js></script>
<script>
/* One-time setup (run once before other code)
 *   adds onreadystatechange to $.ajax options
 *   from https://gist.github.com/chrishow/3023092)
 *   success etc will still fire if provided
 */
$.ajaxPrefilter(function( options, originalOptions, jqXHR ) {
    if ( options.onreadystatechange ) {
        var xhrFactory = options.xhr;
        options.xhr = function() {
            var xhr = xhrFactory.apply( this, arguments );
            function handler() {
                options.onreadystatechange( xhr, jqXHR );
            }
            if ( xhr.addEventListener ) {
                xhr.addEventListener( "readystatechange", handler, false );
            } else {
                setTimeout( function() {
                    var internal = xhr.onreadystatechange;
                    if ( internal ) {
                        xhr.onreadystatechange = function() {
                            handler();
                            internal.apply( this, arguments ); 
                        };
                    }
                }, 0 );
            }
            return xhr;
        };
    }
});

// ----- myReadyStateChange(): this will do my incremental processing -----
var last_start = 0; // using global var for over-simplified example
function myReadyStateChange(xhr /*, jqxhr */) {
    if(xhr.readyState >= 3 && xhr.responseText.length > last_start) {
        var chunk = xhr.responseText.slice(last_start);
        alert('Got chunk: ' + chunk);
        console.log('Got chunk: ', chunk);
        last_start += chunk.length;
    }
}

// ----- call my url and process response incrementally -----
last_start = 0;
$.ajax({
  url: "https://code.jquery.com/jquery-1.5.js", // whatever your target url is goes here
  onreadystatechange: myReadyStateChange
});

</script>

楼主在这里。这个问题是6年前提出的。这个解决方案在2011/2012年是否可行?我已经不再从事这个项目,所以无法测试你的答案。 - Josh
是的,它可以很好地与jQuery 1.5(2011年1月,http://code.jquery.com/jquery-1.5.min.js)一起使用。例如,您只需剪切/粘贴上面的代码即可。 - mwag
我只能相信你。我在多个浏览器中运行了你的完整代码,整个响应都在一个“块”中,所以它并没有真正证明什么。我没有时间再去折腾它了。 - Josh
你应该能够看到它。我将上述内容逐字保存到test.html文件中,并在Chrome中打开它,控制台窗口显示接收到的响应分为两个块。 - mwag

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接