在 Flask 中使用 EventSource

EventSource 是 HTML5 新带进来的特性,用途是可以单方向的接收从服务端推送过来的消息,要双向通信的话估计要使用 WebSocket 的了。

Flask 中需要使用yield来进行 Stream 类型的通信,我们今天的主角是EventSource

我们先来入入门

这是app.py

 from flask import Flask, Response, render_template
 import time 
 app = Flask(__name__) 
 @app.route('/stream') 
 def stream(): 
    def generate(): 
        for i in xrange(10): 
            yield 'data: i = %d' % i time.sleep(.1) 
        
        return Response(generate(), mimetype='text/event-stream') 

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

if __name__ == "__main__": 
    app.run(debug=True)

这是index.html

<html>
 <head lang="en"> 
  <meta charset="UTF-8" /> 
  <title></title> 
  <script src="{{ url_for('static', filename='jquery-2.1.4.js') }}"></script> 
  <script type="application/javascript">
    var source = new EventSource('/stream');
    source.onmessage = function(event) {
        $("#text1").text(event.data);
    }
  </script> 
 </head>
 <body>
  <p id="text1">none</p> 
 </body>
</html>

我们理下上面代码的逻辑,先看index.html这里用new EventSource('/stream')实例化了一个EventSource对象,第一个参数就是目标地址,然后接受消息的时候也就是在onmessage上放了一个回调函数,函数的唯一一个参数就是event,一个事件。

再看app.py里面的,index()就是我们要的首页了噜,stream()也就是待会要访问的目标了,里面还有一个函数名字叫generate,这个函数会返回一个Generator(数据发生器?),具体的效果可以把他单独拿到外面当作一个普通函数运行次看看发生了什么,在这函数的最后一行我们sleep0.1秒,用来模拟一些比较耗时的操作(实际上他是返回了一个generator function,具体可以翻下 Python 的文档。)。

注意这里有个data: %d\n\n,前面的data:和后面的\n\n是绝对必须的……缺了EventSource会认为接收到的是错误的数据,你会调试了半天发现根本没进onmessage里……

我们运行把整个 app 起来看看发生了什么。

我们看见这个p标签里的内容动态地增长到了i = 9

另外一个是event的类型

EventSource是可以定义event的类型的,比如接收到一个ping事件就更新一个时间戳,我们可以这么做。

这是新的app.py

 from flask import Flask, Response, render_template
 import time 

 app = Flask(__name__)

 @app.route('/stream') 
 def stream(): 
    def generate(): 
        timestamp = time.time() 
        for i in xrange(10): 
            yield 'data: i = %d\n\n' % i time.sleep(.1) 
        
        yield 'event: ping\ndata: %d\n\n' % timestamp 
        return Response(generate(), mimetype='text/event-stream') 

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

if __name__ == "__main__": 
    app.run(debug=True)

这是新的index.html

<html>
 <head lang="en"> 
  <meta charset="UTF-8" /> 
  <title></title> 
  <script src="{{ url_for('static', filename='jquery-2.1.4.js') }}"></script> 
  <script type="application/javascript">
    var source = new EventSource('/stream');
    source.onmessage = function(event) {
        $("#text1").text(event.data);
    };
    source.addEventListener('ping',
    function(event) {
        $("#text2").text(event.data);
    });
  </script> 
 </head>
 <body>
  <p id="text1">i</p> 
  <p id="text2">timestamp</p> 
 </body>
</html>

我们发现定义event类型的方式就是在返回的消息前一行加上一个'event: %s' % e_type,然后跟上'data: %s\n\n' % data,我们刷新看下发生了什么。

text1标签上的i = 9了之后,下面那个timestamp就变成了时间戳,看下代码的逻辑是不是这样呢~

其实默认不带event:event的事件类型是message,不信你可以改成这样再试试。

source.addEventListener('message',
function(event) {
    $("#text1").text(event.data);
});

开始折腾

当然你也可以把yield拆成两行来:

yield 'event: ping\n'
yield 'data: %d\n\n' % timestamp

他会接收到数据后自动拼接起来~

然而这并没有什么卯月

靠想象力啊想象力……比如有了这个以后动态实时更新进度条简单了吧~