英文原文地址:http://flask.pocoo.org/docs/1.0/signals/
若有翻译错误或者不尽人意之处,请指出,谢谢~
(新增于版本0.6。)
从Flask版本0.6开始,Flask集成了信号支持。这个支持源于blinker库的支持,并且会在其不可用的时候优雅地退出。
什么是信号呢?当动作在核心框架的其他地方或者在其他Flask扩展中发生时,信号会发送通知,以此来达到应用程序解耦的目的。简单来说,信号允许特定的发送端去通知订阅端到底发生了什么事。
Flask自带了几个信号,而其他扩展组件可能提供更多。还需记住的是,信号是用来通知订阅者的,而不是用来鼓励订阅者修改数据的。你可能注意到,之前讲解过的某些东西就像是这里所说的信号一样,比如内置的修饰符的行为(例如:request_started与before_request()非常相似)。然而,他们的工作原理却不相同。before_request()程序的核心,例如,它是按照特定的顺序执行,并且通过返回一个响应来提前终止请求。相反,所有的信号处理程序都以未定义的顺序执行,并且不能修改任何数据。
信号相比于处理程序最大的优势在于,你仅需一秒钟就可以安全地订阅信号。这些临时订阅对于单元测试是非常有用的。比如你想要知道哪个模板被作为请求的一部分进行渲染得:信号能让你达到这个目的。
1. 订阅信号
为了订阅一个信号,你可以使用信号的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]
注意:connected_to()函数是Blinker1.1版本新增的。
2. 创建信号
如果你想在你的应用程序中使用信号,你可以直接使用blinker库。最常见的方法是在自定义的Namespace中命名信号。这也是最推荐的方式:
from blinker import Namespace
my_singals = Namespace()
现在你就可以像这样创建一个新的信号:
model_saved = my_singals.signal('model-saved')
这里的名称要具有唯一性,这样可以简化调试。你可以使用信号的name属性来访问信号的名称。
给扩展开发者:
如果你正在编写Flask的扩展组件,并且你希望在没有安装blinker时优雅地降级,你可以使用flask.signals.Namespace类。
3. 发送信号
如果你想要发送一个信号,你可以调用send()函数。它接收一个发送者作为第一个参数,并且接收一些即将转发给订阅者的关键字参数:
class Model(object):
...
def save(self):
model_saved.send(self)
尝试着每次都要选择一个合适的发送者对象。如果你有一个类是可以发送信号的,传递self作为发送者对象。如果你在任意的方法中发送了信号,你可以传递current_app._get_current_object()作为发送者对象。
传递代理作为发送者对象:
永远不要为信号传递current_app作为一个发送者对象,而是要使用current_app._get_current_object()来代替。因为current_app是一个代理对象,而不是真正的应用程序对象。
4. 信号和Flask的请求上下文
当接收到信号时,信号能充分支持请求上下文(后面章节会讲到)。上下文局部变量在request_started和request_finished之间是始终可用的,因此你可以使用flask.g和其他需要的东西。注意,不要忘了上一节(发送信号)和request_tearing_down信号所描述的局限性。
5. 基于修饰器的信号订阅
使用Blinker1.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)
6. 核心信号
内置信号请前往Signals进行阅读。