最近要實現這樣一個功能:某個 cgi 處理會很耗時,須要把處理的結果實時的反饋給前端,而不能等到後臺全完成了再咔一下全扔前端,那樣的用戶體驗誰都無法接受。javascript
web 框架選的 flask,這個比較輕量級,看了下官方文檔,剛好有個叫 Streaming from Templates 的功能:html
http://flask.pocoo.org/docs/patterns/streaming/#streaming-from-templates前端
能夠知足需求,它以 generate yield 爲基礎,流式的返回數據到前端。看了下官方的例子貌似很簡單,一筆帶過,我又搜了下 stackoverflow,上面有個老外給了個更加詳盡的例子:Streaming data with Python and Flaskhtml5
http://stackoverflow.com/questions/13386681/streaming-data-with-python-and-flaskjava
文中的答案沒有先後端的數據交互過程,那我就根據本身的需求加個 http 的交互過程了:python
@app.route('/username', methods=['GET', 'POST']) def index(): req =request print req print "111------------" + req.method + "\n" def ggg1(req): print req # the req not my pass into the req.... print "444------------" + req.method + "\n" if req.method == 'POST': if request.form['username']: urlList = request.form['username'].splitlines() i = 0 for url in urlList(): i += 1 resultStr = url print i, resultStr yield i, resultStr print req print "222------------" + req.method + "\n" return Response(stream_template('index.html', data=ggg1(req)))
好吧,這麼一加,噩夢就開始了。。。奇葩的問題出現了:react
要麼第 5 行和第 8 行不等,要麼就是第 9 行報錯:jquery
if request.method == 'POST': # RuntimeError: working outside of request contextgit
繼續在 stackoverflow 上搜索,發現有人遇到了一樣的問題,獲得的建議是在調用前聲明一個 request 上下文:github
with app.test_request_context('/username', method='GET'): index()
折騰了老半天,仍是依舊報錯:RuntimeError: working outside of request context
看起來彷佛是在進入迭代器之前,本來的 request 的生命週期就已經結束了,所以就沒辦法再調用了。
那麼要解決就有 2 種辦法了:
(1)在進入 generationFunc 前將請求複製一份保存下來以供 generationFunc 調用。
(2)利用 app.test_request_context 建立的是一個全新的 request,將數據傳給 generationFunc 使用。
以上這兩種辦法都曾試過,可是因爲理解上的誤差,致使一直未能成功。後來通過 堅實 同窗的指點,才明白箇中原因,問題得以解決。
將請求複製下來但不能直接 req = request 這種形式,這只是給 request 取了個別名,它們是共享引用。正確的代碼以下:
from flask.ctx import _request_ctx_stack global new_request @app.route('/') @app.route('/demo', methods=['POST']) def index(): ctx = _request_ctx_stack.top.copy() new_request = ctx.request def generateFunc(): if new_request.method == 'POST': if new_request.form['digitValue']: num = int(new_request.form['digitValue']) i = 0 for n in xrange(num): i += 1 print "%s:\t%s" % (i, n) yield i, n return Response(stream_template('index.html', data=generateFunc()))
PS: 其實像 _request_ctx_stack 這種如下劃線開頭的變量屬於私有變量,外部是不該該調用的,不過堅實同窗暫時也沒有找到其餘能正式調用到它的方法 ,就先這麼用着吧。
上面的這種寫法:with app.test_request_context('/username', method='GET'):
之因此不能夠是由於 app.test_request_context 建立的是一個全新的 request,它包含的 url, method, headers, form 值都是要在建立時自定義的,它不會把原來的 request 裏的數據帶進來,須要本身傳進去,相似這樣:
with app.test_request_context('/demo', method='POST', data=request.form) as new_context: def generateFunc():
PS: test_request_context 應該是作單元測試用的,用來模仿用戶發起的 HTTP 請求。
它作的事,和你經過瀏覽器提交一個表單或訪問某個網頁是差很少的。
例如你傳給它 url='xxx'、method='post' 等等參數就是告訴它:向 xxx 發起一個 http 請求
這是官方宣稱在 1.0 中實現的一個新特性,http://flask.pocoo.org/docs/api/#flask.copy_current_request_context 看說明應該能夠更加優雅的解決上述問題,
可是試了下貌似不行,多是組件間的兼容性問題。
New in version 0.9.
Note that when you stream data, the request context is already gone the moment the function executes. Flask 0.9 provides you with a helper that can keep the request context around during the execution of the generator:
from flask import stream_with_context, request, Response @app.route('/stream') def streamed_response(): def generate(): yield 'Hello ' yield request.args['name'] yield '!' return Response(stream_with_context(generate()))
Without the stream_with_context() function you would get a RuntimeError at that point.
REF:
(1)flask.request 和 streaming templates 兼容性不是很好,應該儘可能不在 streaming templates 裏調用 request,
把須要的值提早準備好,而後再傳到 templates 裏。這裏也有人遇到一樣的問題:
用 copy_current_request_context 沒有效果應該也是上面這個緣由。
(2)在文檔語焉不詳,同時 google 不到答案的時候,讀源碼或許是最後的選擇,這也是一種能力吧。。。 - _ -
http://stackoverflow.com/questions/13386681/streaming-data-with-python-and-flask
http://flask.pocoo.org/docs/patterns/streaming/
http://stackoverflow.com/questions/8224333/scrolling-log-file-tail-f-animation-using-javascript
http://jsfiddle.net/manuel/zejCD/1/
附堅實同窗的 github 與 sf 地址:
http://segmentfault.com/u/anjianshi
# -*- coding: utf-8 -*- import sys reload(sys) sys.setdefaultencoding('utf-8') from flask import Flask, request, Response app = Flask(__name__) def stream_template(template_name, **context): # http://flask.pocoo.org/docs/patterns/streaming/#streaming-from-templates app.update_template_context(context) t = app.jinja_env.get_template(template_name) rv = t.stream(context) # uncomment if you don't need immediate reaction ##rv.enable_buffering(5) return rv @app.route('/') @app.route('/demo', methods=['POST']) def index(): with app.test_request_context('/demo', method='POST', data=request.form) as new_context: def generateFunc(): new_request = new_context.request if new_request.method == 'POST': if new_request.form['digitValue']: num = int(new_request.form['digitValue']) i = 0 for n in xrange(num): i += 1 print "%s:\t%s" % (i, n) yield i, n return Response(stream_template('index.html', data=generateFunc())) if __name__ == "__main__": app.run(host='localhost', port=8888, debug=True)
<!DOCTYPE html> <html> <head> <title>Bootstrap 101 Template</title> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <!-- Bootstrap --> <!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries --> <!-- WARNING: Respond.js doesn't work if you view the page via file:// --> <!--[if lt IE 9]> <script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script> <script src="https://oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script> <![endif]--> </head> <body> <style> #data { border: 1px solid blue; height: 500px; width: 500px; overflow: hidden; } </style> <script src="http://code.jquery.com/jquery-latest.js"></script> <script> function tailScroll() { var height = $("#data").get(0).scrollHeight; $("#data").animate({ scrollTop: height }, 5); } </script> <form role="form" action="/demo" method="POST"> <textarea class="form-control" rows="1" name="digitValue"></textarea> <button type="submit" class="btn btn-default">Submit</button> </form> <div id="data" style="position:relative;height:400px; overflow-x:auto;overflow-y:auto">nothing received yet</div> {% for i, resultStr in data: %} <script> $("<div />").text("{{ i }}:\t{{ resultStr }}").appendTo("#data") tailScroll(); </script> {% endfor %} <!-- jQuery (necessary for Bootstrap's JavaScript plugins) --> <script src="https://code.jquery.com/jquery.js"></script> <!-- Include all compiled plugins (below), or include individual files as needed --> <script src="/static/dist/js/bootstrap.min.js"></script> </body> </html>
[1] 用Flask實現視頻數據流傳輸
http://python.jobbole.com/80994/
https://github.com/miguelgrinberg/flask-video-streaming
[2] Video Streaming with Flask
http://blog.miguelgrinberg.com/post/video-streaming-with-flask
[3] Flask 的 Context 機制
https://blog.tonyseek.com/post/the-context-mechanism-of-flask/
[4] flask 源碼解析:session