Frappe 内置了一个基于 socket.io 的实时事件 API。由于 socket.io 需要 Node 服务器来运行,我们在主 Web 服务器之外并行运行一个 Node 进程。
客户端 API (JavaScript)
frappe.realtime.on
要在客户端(浏览器)监听实时事件,可以使用 frappe.realtime.on 方法:
frappe.realtime.on('event_name', (data) => {
console.log(data)
})
frappe.realtime.off
停止监听您已订阅的事件:
frappe.realtime.off('event_name')
服务端 API (Python)
frappe.publish_realtime
要从服务端发布实时事件,可以使用 frappe.publish_realtime 方法:
frappe.publish_realtime('event_name', data={'key': 'value'})
frappe.publish_progress
您可以使用此方法在对话框中显示进度条:
frappe.publish_progress(25, title='Some title', description='Some description')
自定义事件处理器 (Python)
注意:此功能仅在夜间版中可用。此功能被视为实验性功能。
您可以通过在自定义应用中创建 your_app/your_app/realtime/handlers.py 文件来实现自定义的实时事件处理器。其语法与 API 白名单非常相似。
from frappe.realtime import realtime
@realtime.on(
"project_subscribe",
frappe_context=False, # open a Frappe context (DB + session) for the handler body
allow_guest=False, # if False, the event is dropped when socket.user == "Guest"
)
默认值:frappe_context=False、allow_guest=False
示例
import frappe
from frappe.realtime import Socket, realtime
@realtime.on("project_subscribe")
def project_subscribe(socket: Socket, project: str) -> None:
if socket.has_permission("Project", project):
socket.join(f"project:{project}")
第一个参数始终是输入的 Socket。其余参数是客户端发送的负载,按位置传递。事件名称 "project_subscribe" 是浏览器通过 frappe.realtime.emit(...) 发出的。
使用 frappe_context=True 的工作示例
使用 frappe_context=True 时,完整的 ORM 在处理器主体中可用 — frappe.get_doc、frappe.local.site、frappe.session.user 都会被填充,并反映已认证的 socket 用户。
此示例处理器加载一个文档,检查权限,并将结果发送回客户端:
@realtime.on("test_get_doc", frappe_context=True)
def test_get_doc(socket: Socket, doctype: str, docname: str) -> None:
import frappe
try:
doc = frappe.get_doc(doctype, docname)
doc.check_permission()
socket.emit(
"test_get_doc_result",
{
"ok": True,
"site": frappe.local.site,
"user": frappe.session.user,
"doctype": doc.doctype,
"name": doc.name,
"modified": str(doc.get("modified")),
},
)
except Exception as e:
socket.emit("test_get_doc_result", {"ok": False, "error": f"{type(e).__name__}: {e}"})
从浏览器端:
frappe.realtime.on("test_get_doc_result", (r) => console.log(r));
frappe.realtime.emit("test_get_doc", "User", "Administrator");
请参阅“权限检查”部分,了解其成本以及何时应优先使用基于 HTTP 的廉价权限检查。
仅当处理器所属的应用安装在连接站点上时,该处理器才会运行。所属应用会自动从导入中检测出来。
您可能需要重启 socketio 服务器才能看到代码更改生效。有关编写自定义事件处理器的更多信息,请参阅 Socket.IO 文档。
Socket 对象
只读身份信息,在连接时填充:
socket.site # the site this socket is on
socket.user # "Guest" for anonymous
socket.user_type # e.g. "System User"
socket.installed_apps # list of installed apps
房间、发送和瞬态状态:
socket.join(room) # add this socket to a room
socket.leave(room) # remove it
socket.emit(event, data=None, room=None) # emit to a room, or to this client if room is None
socket.get(key, default=None) # read transient per-socket state
socket.set(key, value) # persist transient per-socket state (cleared on disconnect)
权限检查
有两种方法可以执行 doctype 权限检查。
A. HTTP(默认,推荐):socket.has_permission(doctype, name) 向 Web 进程发起请求,与核心处理器的行为完全一致。实时进程中不建立数据库连接。成本低。除非有特殊原因,否则请使用此方法。
if socket.has_permission("Project", project):
socket.join(f"project:{project}")
B. 进程内(frappe_context=True):为处理器主体打开一个完整的 Frappe 上下文,以便您可以直接调用 frappe.has_permission(...)、查询数据库等。代价:每个此类事件都会执行 frappe.init -> connect -> set_user -> commit/rollback -> destroy,并强制在实时进程中建立数据库连接。请谨慎使用。
@realtime.on("project_subscribe", frappe_context=True)
def project_subscribe(socket: Socket, project: str) -> None:
if frappe.has_permission("Project", doc=project, ptype="read"):
socket.join(f"project:{project}")
完整参考:++https://github.com/frappe/frappe/blob/develop/frappe/realtime/README.md++
自定义事件处理器 (NodeJS)
从 v16 版本开始可用。 被视为实验性功能。这是之前的实现,将在新版本中逐步淘汰。
Socket.IO 服务器路径;新应用应优先使用上述 Python 处理器。
您可以通过在应用的 realtime 文件夹中创建 handlers.js 文件来实现自定义的实时事件处理器。
此文件需要有一个单一的导出 — 一个在 socket 实例上设置事件处理器的函数。例如,对于一个名为“chat”的应用:
// bench/apps/chat/realtime/handlers.js
function chat_app_handlers(socket) {
socket.on("hello_chat", () => {
console.log("hello world!");
});
}
module.exports = chat_app_handlers;
使用 frappe.realtime.emit("hello_chat") 从客户端代码触发此事件。您可能需要重启 socketio 服务器才能看到代码更改生效。有关编写自定义事件处理器的更多信息,请参阅 Socket.IO 文档。
向客户端推送事件
您不能直接从 Web 进程发送事件 — 您需要通过 Redis 发布,然后实时服务器将其桥接到已连接的 socket。您发布到的房间字符串必须与您的处理器 join 到的房间匹配。
from frappe.realtime import publish_to_room
publish_to_room("project:PROJ-0001", "project_updated", {"status": "Open"})
其他命名的辅助函数,每个都是对 publish_realtime 的轻量封装。
publish_to_user(user, event, message=None)
publish_to_doc(doctype, docname, event, message=None)
publish_to_doctype(doctype, event, message=None)
publish_task_progress(task_id, message=None)
publish_to_website(event, message=None)
publish_to_all(event, message=None)
publish_to_room(room, event, message=None)
完整参考:++https://github.com/frappe/frappe/blob/develop/frappe/realtime/README.md++
自定义客户端
如果你正在开发一个不使用 Desk 界面的 SPA 或移动应用,你可以编写自定义客户端来连接 socket.io 服务器。请参考官方 Socket.IO 客户端文档。
自定义客户端示例:
- gameplan/frontend/src/socket.js
- frappe/socketio_client.js
自定义客户端中的授权
有两种方式可以对 socket.io 服务器的连接进行身份验证:
- Cookies — 在类似浏览器的环境中,连接会自动发送 cookies,socketio 服务器会使用它们进行身份验证。
- 授权头 — 如果 cookies 不可用(例如移动应用),请像 API 请求一样使用
Authorization头。请参阅 REST API 身份验证文档和 Socket.IO extraHeaders。
实现说明
- 实时服务器使用 socket.io 服务器,用 node.js 编写,位于
/realtime目录中。 - 实时客户端是 socket.io 客户端库的封装,位于
public/js/frappe/socketio_client.js中。 - Python 进程通过 Redis 发布-订阅通道将事件发布到 node 服务器。实时服务器订阅 Redis 通道,并重新发布给所有已订阅的客户端。
- 实时服务器是多租户的:所有站点流量都按站点名称进行命名空间隔离。命名空间会动态创建为
/{sitename},其中sitename是站点在sites目录中的文件夹名称(或frappe.local.site)。 - 实时服务器使用主 Frappe Web 服务器来验证连接。SID cookie 或授权头会传递给客户端,并用于确保连接是有效用户,且可以根据权限订阅 DocTypes/文档。
可用房间
| 房间 | 访问权限 |
|---|---|
| all | 所有系统用户默认连接 |
| website | 任何用户(包括访客)均可访问 |
| user:{username} | 按用户分配的房间。无需权限检查即可加入 |
| doctype:{doctype} | 按 DocType 分配的房间。只有具有 DocType 权限的用户才能加入;打开列表/表单视图时自动订阅 |
| doc:{doctype}/{name} | 按文档分配的房间。只有具有文档权限的用户才能加入;打开表单视图时自动订阅 |