本文介紹Flask的訊號機制,講述訊號的用途,並給出建立訊號、訂閱訊號、傳送訊號的方法。
訊號(signals)
Flask訊號(signals, or event hooking)允許特定的傳送端通知訂閱者發生了什麼(既然知道發生了什麼,那我們可以知道接下來該做什麼了)。
Flask提供了一些訊號(核心訊號)且其它的擴充套件提供更多的訊號。訊號是用於通知訂閱者,而不應該鼓勵訂閱者修改資料。相關訊號請查閱文件。
訊號依賴於Blinker庫。
鉤子(hooks)
Flask鉤子(通常出現在藍圖或應用程式現存的方法中,比如一些內建裝飾器,例如before_request
)不需要Blinker庫並且允許你改變請求物件(request
)或者響應物件(response
)。這些改變了應用程式(或者藍圖)的行為。比如before_request()
裝飾器。
訊號看起來和鉤子做同樣的事情。然而在工作方式上它們存在不同。譬如核心的before_request()
處理程式以特定的順序執行,並且可以在返回響應之前放棄請求。相比之下,所有的訊號處理器是無序執行的,並且不修改任何資料。
一般來說,鉤子用於改變行為(比如,身份驗證或錯誤處理),而訊號用於記錄事件(比如記錄日誌)。
建立訊號
因為訊號依賴於Blinker庫,請確保已經安裝。
如果你要在自己的應用中使用訊號,你可以直接使用Blinker庫。最常見的使用情況是命名一個自定義的Namespace
的訊號。這也是大多數時候推薦的做法:
1 2 |
from blinker import Namespace my_signals = Namespace() |
現在你可以像這樣建立新的訊號:
1 |
model_saved = my_signals.signal('model-saved') |
這裡使用唯一的訊號名並且簡化除錯。可以用name
屬性來訪問訊號名。
對擴充套件開發者:
如果你正在編寫一個Flask擴充套件,你想優雅地減少缺少Blinker安裝的影響,你可以這樣做使用flask.signals.Namespace
類。
訂閱訊號
你可以使用訊號的connect()
方法來訂閱訊號。第一個引數是訊號發出時要呼叫的函式,第二個引數是可選的,用於確定訊號的傳送者。一個訊號可以擁有多個訂閱者。你可以用disconnect()
方法來退訂訊號。
對於所有的核心Flask訊號,傳送者都是發出訊號的應用。當你訂閱一個訊號,請確保也提供一個傳送者,除非你確實想監聽全部應用的訊號。這在你開發一個擴充套件的時候尤其正確。
比如這裡有一個用於在單元測試中找出哪個模板被渲染和傳入模板的變數的助手上下文管理器:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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) |
現在可以輕鬆地與測試客戶端配對:
1 2 3 4 5 6 7 |
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()
) ,它允許你臨時地用它自己的上下文管理器把函式訂閱到訊號。因為上下文管理器的返回值不能按那種方法給定,則需要把列表作為引數傳入:
1 2 3 4 5 6 |
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) |
上面的例子看起來像這樣:
1 2 3 4 |
templates = [] with captured_templates(app, templates, **extra): ... template, context = templates[0] |
傳送訊號
如果你想要傳送訊號,你可以通過呼叫 send()
方法來達到目的。它接受一個發件者作為第一個引數和一些可選的被轉發到訊號使用者的關鍵字引數:
1 2 3 4 5 |
class Model(object): ... def save(self): model_saved.send(self) |
永遠嘗試選擇一個合適的傳送者。如果你有一個發出訊號的類,把 self
作為傳送者。如果你從一個隨機的函式發出訊號,把current_app._get_current_object()
作為傳送者。
基於訊號訂閱的裝飾器
在Blinker 1.1中通過使用新的connect_via()
裝飾器你也能夠輕易地訂閱訊號:
1 2 3 4 5 |
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
)一起呼叫。
訊號傳送:
1 2 3 4 5 |
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 |
訂閱示例:
1 2 3 4 5 6 7 |
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中定義的訊號,當使用者成功登入之後,這個訊號會被髮送。
傳送訊號:
1 2 |
# Send user_logged_in signal user_logged_in.send(current_app._get_current_object(), user=user) |
下面這個例子追蹤登入次數和登入IP:
1 2 3 4 5 6 7 8 9 10 11 |
# 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() |
小結
訊號可以讓你在一瞬間安全地訂閱它們。例如,這些臨時的訂閱對測試很有幫助。使用訊號時,不要讓訊號訂閱者(接收者)發生異常,因為異常會造成程式中斷。