訊號(signals)
Flask訊號(signals, or event hooking)允許特定的發送端通知訂閱者發生了什麼(既然知道發生了什麼,那我們可以知道接下來該做什麼了)。
Flask提供了一些訊號(核心訊號)且其它的擴充提供更多的訊號。訊號是用於通知訂閱者,而不應該鼓勵訂閱者修改資料。相關訊號請查閱文檔。
訊號依賴於Blinker庫。
鉤子(hooks)
Flask鉤子(通常出現在藍圖或應用程式現存的方法中,比如一些內建裝飾器,例如before_request)不需要Blinker庫並且允許你改變請求對象(request)或者響應對象(response)。這些改變了應用程式(或者藍圖)的行為。比如before_request()裝飾器。
訊號看起來和鉤子做同樣的事情。然而在工作方式上它們存在不同。譬如核心的before_request()處理常式以特定的順序執行,並且可以在返迴響應之前放棄請求。相比之下,所有的訊號處理器是無序執行的,並且不修改任何資料。
一般來說,鉤子用於改變行為(比如,身分識別驗證或錯誤處理),而訊號用於記錄事件(比如記錄日誌)。
建立訊號
因為訊號依賴於Blinker庫,請確保已經安裝。
如果你要在自己的應用中使用訊號,你可以直接使用Blinker庫。最常見的使用方式是命名一個自訂的Namespace的訊號。這也是大多數時候推薦的做法:
複製代碼 代碼如下:
from blinker import Namespace
my_signals = Namespace()
現在你可以像這樣建立新的訊號:
複製代碼 代碼如下:
model_saved = my_signals.signal('model-saved')
這裡使用唯一的訊號名並且簡化調試。可以用name屬性來訪問訊號名。
對擴充開發人員:
如果你正在編寫一個Flask擴充,你想優雅地減少缺少Blinker安裝的影響,你可以這樣做使用flask.signals.Namespace類。
訂閱訊號
你可以使用訊號的connect()方法來訂閱訊號。第一個參數是訊號發出時要調用的函數,第二個參數是可選的,用於確定訊號的寄件者。一個訊號可以擁有多個訂閱者。你可以用disconnect()方法來退訂訊號。
對於所有的核心Flask訊號,寄件者都是發出訊號的應用。當你訂閱一個訊號,請確保也提供一個寄件者,除非你確實想監聽全部應用的訊號。這在你開發一個擴充的時候尤其正確。
比如這裡有一個用於在單元測試中找出哪個模板被渲染和傳入模板的變數的助手上下文管理器:
複製代碼 代碼如下:
from flask import template_rendered
from contextlib import contextmanager
@contextmanager
def captured_templates(app):
recorded = []
def record(sender, template, context, **extra):
recorded.append((template, context))
template_rendered.connect(record, app)
try:
yield recorded
finally:
template_rendered.disconnect(record, app)
現在可以輕鬆地與測試用戶端配對:
複製代碼 代碼如下:
with captured_templates(app) as templates:
rv = app.test_client().get('/')
assert rv.status_code == 200
assert len(templates) == 1
template, context = templates[0]
assert template.name == 'index.html'
assert len(context['items']) == 10
確保訂閱使用了一個額外的 **extra 參數,這樣當 Flask 對訊號引入新參數時你的調用不會失敗。
代碼中,從 with 塊的應用 app 中流出的渲染的所有模板現在會被記錄到templates 變數。無論何時模板被渲染,模板對象的上下文中添加上它。
此外,也有一個方便的助手方法(connected_to()) ,它允許你臨時地用它自己的上下文管理器把函數訂閱到訊號。因為上下文管理器的傳回值不能按那種方法給定,則需要把列表作為參數傳入:
複製代碼 代碼如下:
from flask import template_rendered
def captured_templates(app, recorded, **extra):
def record(sender, template, context):
recorded.append((template, context))
return template_rendered.connected_to(record, app)
上面的例子看起來像這樣:
複製代碼 代碼如下:
templates = []
with captured_templates(app, templates, **extra):
...
template, context = templates[0]
發送訊號
如果你想要發送訊號,你可以通過調用 send() 方法來達到目的。它接受一個發件者作為第一個參數和一些可選的被轉寄到訊號使用者的關鍵字參數:
複製代碼 代碼如下:
class Model(object):
...
def save(self):
model_saved.send(self)
永遠嘗試選擇一個合適的寄件者。如果你有一個發出訊號的類,把 self 作為寄件者。如果你從一個隨機的函數發出訊號,把current_app._get_current_object()作為寄件者。
基於訊號訂閱的裝飾器
在Blinker 1.1中通過使用新的connect_via()裝飾器你也能夠輕易地訂閱訊號:
複製代碼 代碼如下:
from flask import template_rendered
@template_rendered.connect_via(app)
def when_template_rendered(sender, template, context, **extra):
print 'Template %s is rendered with %s' % (template.name, context)
舉例
模板渲染
template_rendered訊號是Flask核心訊號。
當一個模版成功地渲染,這個訊號會被發送。這個訊號與模板執行個體 template 和內容相關的詞典(名為 context )一起調用。
訊號發送:
複製代碼 代碼如下:
def _render(template, context, app):
"""Renders the template and fires the signal"""
rv = template.render(context)
template_rendered.send(app, template=template, context=context)
return rv
訂閱樣本:
複製代碼 代碼如下:
def log_template_renders(sender, template, context, **extra):
sender.logger.debug('Rendering template "%s" with context %s',
template.name or 'string template',
context)
from flask import template_rendered
template_rendered.connect(log_template_renders, app)
帳號追蹤
user_logged_in是Flask-User中定義的訊號,當使用者成功登入之後,這個訊號會被發送。
發送訊號:
複製代碼 代碼如下:
# Send user_logged_in signal
user_logged_in.send(current_app._get_current_object(), user=user)
下面這個例子追蹤登入次數和登入IP:
複製代碼 代碼如下:
# This code has not been tested
from flask import request
from flask_user.signals import user_logged_in
@user_logged_in.connect_via(app)
def _track_logins(sender, user, **extra):
user.login_count += 1
user.last_login_ip = request.remote_addr
db.session.add(user)
db.session.commit()
小結
訊號可以讓你在一瞬間安全地訂閱它們。例如,這些臨時的訂閱對測試很有協助。使用訊號時,不要讓訊號訂閱者(接收者)發生異常,因為異常會造成程式中斷。