在更新时显示从 Flask 视图流式传输的数据

IT技术 javascript python flask stream
2021-02-05 15:32:41

我有一个视图可以生成数据并实时传输它。我不知道如何将这些数据发送到我可以在 HTML 模板中使用的变量。我当前的解决方案只是在数据到达时将数据输出到空白页面,这可行,但我想将其包含在带有格式的更大页面中。如何在数据流式传输到页面时更新、格式化和显示数据?

import flask
import time, math

app = flask.Flask(__name__)

@app.route('/')
def index():
    def inner():
        # simulate a long process to watch
        for i in range(500):
            j = math.sqrt(i)
            time.sleep(1)
            # this value should be inserted into an HTML template
            yield str(i) + '<br/>\n'
    return flask.Response(inner(), mimetype='text/html')

app.run(debug=True)
2个回答

您可以在响应中流式传输数据,但不能按照您描述的方式动态更新模板。模板在服务器端渲染一次,然后发送到客户端。

一种解决方案是使用 JavaScript 读取流式响应并在客户端输出数据。用于XMLHttpRequest向将流式传输数据的端点发出请求。然后定期从流中读取,直到完成。

这引入了复杂性,但允许直接更新页面并完全控制输出的外观。以下示例通过显示当前值和所有值的日志来演示。

这个例子假设了一个非常简单的消息格式:一行数据,后跟一个换行符。只要有一种方法可以识别每条消息,就可以根据需要进行复杂处理。例如,每个循环都可以返回一个客户端解码的 JSON 对象。

from math import sqrt
from time import sleep
from flask import Flask, render_template

app = Flask(__name__)

@app.route("/")
def index():
    return render_template("index.html")

@app.route("/stream")
def stream():
    def generate():
        for i in range(500):
            yield "{}\n".format(sqrt(i))
            sleep(1)

    return app.response_class(generate(), mimetype="text/plain")
<p>This is the latest output: <span id="latest"></span></p>
<p>This is all the output:</p>
<ul id="output"></ul>
<script>
    var latest = document.getElementById('latest');
    var output = document.getElementById('output');

    var xhr = new XMLHttpRequest();
    xhr.open('GET', '{{ url_for('stream') }}');
    xhr.send();
    var position = 0;

    function handleNewData() {
        // the response text include the entire response so far
        // split the messages, then take the messages that haven't been handled yet
        // position tracks how many messages have been handled
        // messages end with a newline, so split will always show one extra empty message at the end
        var messages = xhr.responseText.split('\n');
        messages.slice(position, -1).forEach(function(value) {
            latest.textContent = value;  // update the latest value in place
            // build and append a new item to a list to log all output
            var item = document.createElement('li');
            item.textContent = value;
            output.appendChild(item);
        });
        position = messages.length - 1;
    }

    var timer;
    timer = setInterval(function() {
        // check the response for new data
        handleNewData();
        // stop checking once the response has ended
        if (xhr.readyState == XMLHttpRequest.DONE) {
            clearInterval(timer);
            latest.textContent = 'Done';
        }
    }, 1000);
</script>

An<iframe>可用于显示流式 HTML 输出,但它有一些缺点。框架是一个单独的文档,这增加了资源的使用。由于它仅显示流式数据,因此可能不容易像页面的其余部分一样对其进行样式设置。它只能附加数据,因此长输出将呈现在可见滚动区域下方。它不能响应每个事件修改页面的其他部分。

index.html使用指向stream端点的框架呈现页面该框架的默认尺寸相当小,因此您可能需要进一步设置样式。使用render_template_string,它知道转义变量,为每个项目呈现 HTML(或render_template与更复杂的模板文件一起使用)。可以生成初始行以首先在框架中加载 CSS。

from flask import render_template_string, stream_with_context

@app.route("/stream")
def stream():
    @stream_with_context
    def generate():
        yield render_template_string('<link rel=stylesheet href="{{ url_for("static", filename="stream.css") }}">')

        for i in range(500):
            yield render_template_string("<p>{{ i }}: {{ s }}</p>\n", i=i, s=sqrt(i))
            sleep(1)

    return app.response_class(generate())
<p>This is all the output:</p>
<iframe src="{{ url_for("stream") }}"></iframe>

晚了 5 年,但这实际上可以按照您最初尝试的方式完成,javascript 完全没有必要(编辑:接受的答案的作者在我写完这篇文章后添加了 iframe 部分)。您只需要将输出嵌入为<iframe>

from flask import Flask, render_template, Response
import time, math

app = Flask(__name__)

@app.route('/content') # render the content a url differnt from index
def content():
    def inner():
        # simulate a long process to watch
        for i in range(500):
            j = math.sqrt(i)
            time.sleep(1)
            # this value should be inserted into an HTML template
            yield str(i) + '<br/>\n'
    return Response(inner(), mimetype='text/html')

@app.route('/')
def index():
    return render_template('index.html.jinja') # render a template at the index. The content will be embedded in this template

app.run(debug=True)

然后 'index.html.jinja' 文件将包含一个<iframe>以内容 url 作为 src 的内容,类似于:

<!doctype html>
<head>
    <title>Title</title>
</head>
<body>
    <div>
        <iframe frameborder="0" noresize="noresize"
     style='background: transparent; width: 100%; height:100%;' src="{{ url_for('content')}}"></iframe>
    </div>
</body>


渲染render_template_string()时应使用用户提供的数据来渲染内容以避免注入攻击。但是,我将其排除在示例之外,因为它增加了额外的复杂性,超出了问题的范围,与 OP 无关,因为他没有流式传输用户提供的数据,并且与大量数据无关大多数人看到这篇文章,因为流式传输用户提供的数据是一种极端情况,很少有人需要这样做。