- 前端
- 跨平台
- 桌面应用
- 移动开发
【免费下载链接】flet
Build realtime web, mobile and desktop apps in Python only. No frontend experience required.
在 Flet 中,每个用户打开应用都会对应一个独立的"会话"(Session),不同会话之间默认互不可见。如果你需要构建聊天室、实时通知、协作编辑这类"一个用户发消息、所有其他用户立刻看到"的功能,就需要一套跨会话的通信机制。Flet 提供了内置的PubSub(发布/订阅)组件,而 PubSubClient 正是这套机制中面向应用开发者最常用的入口——它是绑定到当前页面会话的客户端门面,所有发布、订阅、退订操作都围绕当前会话自动完成。
读完本文你将掌握:如何通过page.pubsub获取客户端、send_all/send_others/subscribe/subscribe_topic等全部方法的语义与适用场景、一个可直接运行的聊天应用完整示例,以及底层PubSubHub的实现原理与会话清理机制。
PubSubClient 是什么:会话级的 PubSub 门面
PubSubClient是 Flet 内置 PubSub 机制中绑定到单个会话的客户端对象。从源码的类注释可以直接看到它的定位(pubsub_client.py):
Session-scoped facade over
PubSubHub. This client binds all pub/sub operations to one session ID so callers can publish and subscribe without passing their session identity explicitly on each call.
翻译过来就是:它是PubSubHub的会话级门面(Facade)。所有 pub/sub 操作都被绑定到一个 session ID 上,调用者在每次调用时无需手动传递会话身份。
构造函数需要两个参数(pubsub_client.py):
def __init__(self, pubsub: PubSubHub, session_id: str):pubsub:进程内共享的PubSubHub实例,负责真正的消息路由;session_id:当前会话的唯一标识,send_others(发给别人)、unsubscribe(按会话退订)等操作都依赖它来区分"自己"和"别人"。
在常规的 Flet 服务端应用中,你不需要手动构造PubSubClient。会话建立时,Flet 会在 session.py 中自动完成装配:
self.__pubsub_client = PubSubClient(conn.pubsubhub, self.__id)其中self.__id是通过random_string(16)生成的 16 位随机会话 ID(session.py),conn.pubsubhub则是连接层持有的进程级PubSubHub。而Page对象暴露了pubsub属性,直接透传会话客户端(page.py):
@property def pubsub(self) -> "PubSubClient": """The PubSub client for the current page.""" return self.session.pubsub_client所以在应用代码里,拿到当前页面客户端只需一行:
client = page.pubsub九个核心方法全解析
PubSubClient共提供 9 个方法,可分为发布、订阅、退订三大类。下表汇总了全部方法与语义:
| 分类 | 方法 | 作用 | 回调参数 |
|---|---|---|---|
| 发布 | send_all(message) | 向所有会话广播全局消息 | — |
| 发布 | send_all_on_topic(topic, message) | 向指定主题的所有订阅者广播 | — |
| 发布 | send_others(message) | 向除本会话外的所有会话广播全局消息 | — |
| 发布 | send_others_on_topic(topic, message) | 向主题订阅者广播,但排除本会话 | — |
| 订阅 | subscribe(handler) | 订阅全局广播消息 | (message) |
| 订阅 | subscribe_topic(topic, handler) | 订阅某个主题 | (topic, message) |
| 退订 | unsubscribe() | 移除本会话的全部全局订阅 | — |
| 退订 | unsubscribe_topic(topic) | 移除本会话对指定主题的订阅 | — |
| 退订 | unsubscribe_all() | 移除本会话所有(全局 + 主题)订阅 | — |
发布消息:send_all 系列
四个发送方法全部转发到共享的PubSubHub,其中send_others系列由客户端自动补上本会话 ID 实现"排除自己"(pubsub_client.py):
def send_all(self, message: Any): self.__pubsub.send_all(message) def send_all_on_topic(self, topic: str, message: Any): self.__pubsub.send_all_on_topic(topic, message) def send_others(self, message: Any): self.__pubsub.send_others(self.__session_id, message) def send_others_on_topic(self, topic: str, message: Any): self.__pubsub.send_others_on_topic(self.__session_id, topic, message)message的类型是Any,可以是字符串、字典、列表,甚至是自定义对象。注意send_others系只排除发起者所在会话,其他所有会话(哪怕也有多个浏览器标签)都会收到。
订阅消息:subscribe 与 subscribe_topic
subscribe(handler)注册当前会话的全局消息处理器,处理器只接收一个位置参数message(pubsub_client.py):
def subscribe(self, handler: Callable[[Any], Any]): """The handler is invoked with one positional argument: `message`.""" self.__pubsub.subscribe(self.__session_id, handler)subscribe_topic(topic, handler)则注册主题订阅,处理器接收两个位置参数(topic, message)(pubsub_client.py):
def subscribe_topic(self, topic: str, handler: Callable[[str, Any], Any]): """The handler is invoked with two positional arguments: `(topic, message)`.""" self.__pubsub.subscribe_topic(self.__session_id, topic, handler)从PubSubHub的实现看,处理器既可以是普通同步函数,也可以是async协程函数(pubsub_hub.py)。同步函数在事件循环的线程池执行器中运行,异步函数则通过run_coroutine_threadsafe调度回事件循环——这意味着你可以在回调里安全地操作 UI 控件并调用page.update()。
退订:unsubscribe 系列
三个退订方法的粒度不同(pubsub_client.py):
unsubscribe():只清掉本会话的全局广播订阅,主题订阅不受影响;unsubscribe_topic(topic):只清掉本会话对指定主题的订阅;unsubscribe_all():一次性清掉本会话的全部全局与主题订阅。
典型使用流程:订阅 → 发布 → 退订
官方 PubSub 教程 给出了一个典型的生命周期范式,核心步骤恰好覆盖了客户端的三大类方法:
- 应用会话启动时调用
subscribe()(订阅广播)或subscribe_topic()(订阅主题); - 某个事件发生时(如"发送"按钮点击)调用
send_all()(广播)或send_all_on_topic()(按主题发送); - 某个事件发生时(如"离开"按钮点击)调用
unsubscribe()或unsubscribe_topic(); - 页面关闭时在
page.on_close中调用unsubscribe_all()清理一切订阅。
实战示例:一个基于 PubSub 的完整聊天应用
下面这个聊天应用来自官方教程,但特意用到了send_others(只发给别人,自己那条消息由本地逻辑直接追加),避免"自己发自己收"带来的重复渲染问题。它展示了订阅、发布、退订的完整闭环:
import flet as ft def main(page: ft.Page): page.title = "Flet Chat" # 1. 订阅全局广播消息:别人发来的消息追加到消息列表 def on_message(msg): messages.controls.append(ft.Text(msg)) page.update() page.pubsub.subscribe(on_message) # 2. 发送消息:发给所有其他会话,自己本地直接显示 def send_click(e): page.pubsub.send_others(f"{user.value}: {message.value}") messages.controls.append(ft.Text(f"{user.value}: {message.value}")) message.value = "" page.update() # 3. 离开聊天:退订广播消息 def leave_click(e): page.pubsub.unsubscribe() messages.controls.append(ft.Text("You left the chat")) page.update() messages = ft.Column() user = ft.TextField(hint_text="Your name", width=150) message = ft.TextField(hint_text="Your message...", expand=True) send = ft.Button("Send", on_click=send_click) leave = ft.Button("Leave", on_click=leave_click) page.add( messages, ft.Row(controls=[user, message, send, leave]), ) ft.run(main, view=ft.AppView.WEB_BROWSER)两个浏览器窗口打开同一应用即为两个会话,在其中一个发送消息,另一个会实时收到——这正是PubSubClient最典型的落地场景(效果见下图):
底层原理:PubSubHub 的线程安全路由
PubSubClient只是薄薄的一层门面,真正的消息路由在PubSubHub中完成(pubsub_hub.py):
Thread-safe in-memory pub/sub router scoped to a Flet server process.
理解它有助于把握客户端行为的上限与边界。核心要点如下:
1. 三份索引结构(pubsub_hub.py):
self.__subscribers: dict[str, set[handler]] # session_id -> 全局handler集合 self.__topic_subscribers: dict[str, dict[str, set]] # topic -> session_id -> handler集合 self.__subscriber_topics: dict[str, dict[str, set]] # session_id -> topic -> handler集合(反向索引)前两份用于高效路由发布,第三份反向索引让"按会话退订"(unsubscribe_all)能在常数时间内找到该会话订阅过的全部主题。
2. 线程安全:整个 hub 用一把threading.Lock保护所有读写。在 Pyodide(浏览器内运行)环境下则替换为无操作锁NopeLock,因为该环境不存在多线程竞争(pubsub_hub.py)。
3. 同步与异步处理器分派(pubsub_hub.py):
if inspect.iscoroutinefunction(handler): asyncio.run_coroutine_threadsafe(handler(*args), self.__loop) else: if self.__executor: self.__loop.call_soon_threadsafe( self.__loop.run_in_executor, self.__executor, handler, *args ) else: handler(*args)- 异步 handler:
run_coroutine_threadsafe提交到事件循环执行; - 同步 handler:有 executor 时提交到线程池,避免阻塞事件循环;无 executor 时在当前线程内联调用;
- 未配置事件循环:任何发送操作都会抛出
RuntimeError: PubSub event loop is not set。
4. 作用域:hub 是进程内内存路由。每个 Flet 服务端进程(TCP/Unix Socket 传输的FletSocketServer,见 flet_socket_server.py;桌面/移动端 Dart Bridge 传输的FletDartBridgeServer,见 flet_dart_bridge_server.py)在启动时都会创建PubSubHub(loop=loop, executor=executor)。而 Pyodide 模式下(pyodide_connection.py)创建的是不带 loop/executor 的裸 hub。这意味着消息无法跨进程传递——若部署了多个服务进程(如多 worker 的 Web 服务),各进程间的 PubSub 是隔离的,这一点在设计架构时需特别注意。
会话生命周期与自动清理
PubSubClient的订阅与会话生命周期强绑定,无需担心"僵尸订阅":
- 每个会话创建时自动获得自己的客户端(session.py),会话 ID 全局唯一;
- 会话关闭(
Session.close())时,Flet 会自动调用self.__pubsub_client.unsubscribe_all()清掉该会话的全部订阅(session.py); - 因此,即使开发者忘记在
page.on_close里手动退订,服务端会话回收时也会兜底清理,不会把消息发给已断开的会话。
不过官方教程仍然建议在page.on_close中显式调用unsubscribe_all(),以尽早释放回调闭包引用的对象,避免内存滞留。
使用建议与边界
- 全局广播 vs 主题:所有会话都该收到的用
send_all/subscribe;只想让特定分组(如"房间 1"的成员)收到的用send_all_on_topic/subscribe_topic。主题名就是普通字符串,由你自行约定命名规范(如"room:101")。 - 自己是否接收:默认
send_all会把消息发回发起者自己;若回调逻辑会把消息追加到 UI,建议用send_others避免重复显示,或让回调按会话判断来源。 - 处理器重入:同一会话重复
subscribe同一 handler 时,由于 handler 存放在set中,重复注册会被自动去重(pubsub_hub.py),无需担心重复回调。 - 进程内限定:PubSub 不跨进程、不跨机器,属于轻量级内存消息总线;需要跨服务实例的分布式消息,应引入独立的消息中间件。
两个类均从flet顶层包直接导出(PubSubClient、PubSubHub,见init.py),模块级导出定义在 pubsub/init.py,其中 hub 的完整 API 可参考 PubSubHub 文档。掌握PubSubClient,你就掌握了 Flet 多用户实时应用的消息骨架。
- 前端
- 跨平台
- 桌面应用
- 移动开发
【免费下载链接】flet
Build realtime web, mobile and desktop apps in Python only. No frontend experience required.
相关推荐
PubSubClient消息处理深度解析:发布与订阅的完整流程
PubSubClient消息处理深度解析:发布与订阅的完整流程 PubSubClient是一个专为Arduino平台设计的MQTT客户端库,它提供了完整的发布订
物联网消息队列Azure Service Bus Python SDK 实战指南:队列、主题订阅与会话消息全解析
Azure Service Bus Python SDK 实战指南:队列、主题订阅与会话消息全解析 本指南基于 agentic awesome skills 仓
AI 技能AI 插件AnotherRedisDesktopManager 发布订阅功能:实时消息接收与发送
AnotherRedisDesktopManager 发布订阅功能:实时消息接收与发送 引言:Redis 发布订阅的应用场景与痛点 你是否在分布式系统开发中遇到
数据库客户端桌面应用
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考