系统通知 Notifications
本教程共 42 篇 · 第 18 篇 · 更新于 2026-08-03
18. 系统通知 Notifications
本节目标
- 会初始化通知系统并按平台处理授权
- 能发一条最基础的通知
- 会用分类和动作做交互式通知(按钮、文本回复)
- 能在 Go 侧接收用户点击,并转成事件给前端
- 知道三大平台的差异和退出时的清理
18-1 通知系统概览
通知是系统级的,不依赖你的窗口。即使应用最小化到托盘,通知照样从系统右下角(或 macOS 右上角)弹出来。Wails 的 Notification 模块把 Windows 的 toast、macOS 的通知中心、Linux 的桌面通知统一封装了。
和对话框不同,通知在 JS 运行时里是「部分支持」:发送、注册分类、移除这些方法前端能直接调,但接收用户点击的 OnNotificationResponse 只在 Go 侧有。前端想响应点击,得靠事件系统——后面 18-5 会讲。
先说初始化。通知不是开箱即用的,要先 InitializeNotifications,建议在 OnStartup 里做:
func (a *App) startup(ctx context.Context) {
a.ctx = ctx
if err := runtime.InitializeNotifications(ctx); err != nil {
// macOS 上 bundle identifier 没配可能失败
runtime.LogError(ctx, "通知初始化失败: "+err.Error())
}
}
初始化失败通常出在 macOS:如果打包时没配置 Info.plist 的 bundle identifier,系统拒绝授权。Windows 和 Linux 基本不会失败。
18-2 授权与可用性
发之前先确认平台支持:
if !runtime.IsNotificationAvailable(a.ctx) {
// Windows/Linux 永远返回 true;macOS 检查 10.14+
return
}
macOS 有系统的通知权限弹窗,必须主动申请。其他平台不需要。
authorized, err := runtime.CheckNotificationAuthorization(a.ctx)
if err != nil {
return
}
if !authorized {
authorized, err = runtime.RequestNotificationAuthorization(a.ctx)
if err != nil || !authorized {
// 用户拒绝了,只能退而求其次(比如应用内提示)
return
}
}
CheckNotificationAuthorization 先查现状,没授权再 RequestNotificationAuthorization 弹系统请求。Windows 和 Linux 这两个函数永远返回 true,写上去也无害。
WarningmacOS 的权限弹窗只有一次机会。用户点了「不允许」之后,应用内再调用
RequestNotificationAuthorization也不会再弹,只能引导用户去系统设置里手动开。所以首次申请前最好先用一个应用内说明页铺垫,别冷不丁弹系统框。
18-3 发送基础通知
最简单的通知只要 ID、Title、Body:
err := runtime.SendNotification(a.ctx, runtime.NotificationOptions{
ID: "meeting-001",
Title: "团队会议",
Body: "还有 30 分钟开始",
})
if err != nil {
runtime.LogError(a.ctx, "发送失败: "+err.Error())
}
NotificationOptions 的字段:
| 字段 | 作用 | 平台 |
|---|---|---|
| ID | 通知唯一标识 | 全平台 |
| Title | 主标题 | 全平台 |
| Subtitle | 副标题 | 仅 Mac/Linux |
| Body | 正文 | 全平台 |
| CategoryID | 交互分类标识 | 全平台 |
| Data | 附带自定义数据 | 全平台 |
Subtitle 在 Windows 上会被忽略,写了也不显示。所以副标题里别放关键信息,正文 Body 才是三平台都可见的。
Tip通知适合「告知」,不适合「必须确认」。如果用户不点通知,你的逻辑要能承受。需要强制用户回应(比如退出前存盘确认),应该用上一章的消息框 MessageDialog,而不是通知。
18-4 交互通知:分类与动作
单纯的提示不够,很多场景要用户点「是/否」「打开/忽略」。这就用到交互通知,分两步:先注册一个分类,再用这个分类发通知。
注册分类,定义动作按钮和可选的回复框:
category := runtime.NotificationCategory{
ID: "msg-category",
Actions: []runtime.NotificationAction{
{ID: "OPEN", Title: "打开"},
{ID: "ARCHIVE", Title: "归档", Destructive: true}, // Mac 上显示红色
},
HasReplyField: true,
ReplyPlaceholder: "输入回复…",
ReplyButtonTitle: "回复",
}
if err := runtime.RegisterNotificationCategory(a.ctx, category); err != nil {
runtime.LogError(a.ctx, "注册分类失败: "+err.Error())
}
NotificationAction 的 Destructive 字段只在 macOS 生效,被标红的按钮通常表示破坏性操作(删除、忽略)。Windows 和 Linux 忽略它。
发交互通知时带上 CategoryID:
err := runtime.SendNotificationWithActions(a.ctx, runtime.NotificationOptions{
ID: "msg-001",
Title: "新消息",
Body: "有空一起吃午饭?",
CategoryID: "msg-category",
})
如果 CategoryID 不存在或留空,SendNotificationWithActions 会退化为发一条普通通知,不会报错。
Tip通知还能带自定义数据,在响应里原样返回。比如把业务 ID 塞进
Data,用户点了通知你就知道点的是哪条:
runtime.SendNotification(a.ctx, runtime.NotificationOptions{
ID: "task-001",
Title: "任务提醒",
Body: "该提交周报了",
Data: map[string]interface{}{"taskId": "weekly-123"},
})
18-5 处理用户响应
这是 JS 运行时的盲点。OnNotificationResponse 只能在 Go 侧注册回调:
runtime.OnNotificationResponse(a.ctx, func(result runtime.NotificationResult) {
if result.Error != nil {
runtime.LogError(a.ctx, "响应出错: "+result.Error.Error())
return
}
resp := result.Response
runtime.LogInfo(a.ctx, "通知 "+resp.ID+" 被点击,动作: "+resp.ActionIdentifier)
if resp.ActionIdentifier == "TEXT_REPLY" {
runtime.LogInfo(a.ctx, "用户回复: "+resp.UserText)
}
// 转成事件,让前端收到
runtime.EventsEmit(a.ctx, "notification-response", resp)
})
resp.ActionIdentifier 有几个特殊值:DEFAULT_ACTION 表示用户点了通知本体(不是按钮),TEXT_REPLY 表示通过回复框提交文本,此时 resp.UserText 是用户输入的内容。
前端听这个事件就行:
import { EventsOn } from "../wailsjs/runtime/runtime";
useEffect(() => {
const unsub = EventsOn("notification-response", (resp: any) => {
switch (resp.actionIdentifier) {
case "OPEN":
openDetail(resp.userInfo?.taskId);
break;
case "TEXT_REPLY":
console.log("用户回复:", resp.userText);
break;
}
});
return unsub;
}, []);
EventsOn 返回的是取消订阅函数,组件卸载时调用即可,避免重复监听。
18-6 平台差异与清理
三大平台能力不一,列个对照:
- macOS:功能最全,支持副标题、文本输入、红色破坏性按钮、深色浅色自适应。但要授权、要公证(notarization)才能分发。
- Windows:用 toast,支持文本输入和高 DPI,不支持副标题。无权限系统,开箱即用。
- Linux:依赖桌面环境(GNOME/KDE/XFCE 等),支持副标题,但不支持文本输入。没有原生通知后端时可能根本不弹。
退出时要清理通知资源,尤其 Linux 会占用 D-Bus 连接:
func (a *App) shutdown(ctx context.Context) {
runtime.CleanupNotifications(ctx)
}
其它可选的清理方法:RemoveNotificationCategory 注销分类、RemovePendingNotification / RemoveAllPendingNotifications 撤掉待显示的通知、RemoveDeliveredNotification / RemoveAllDeliveredNotifications 清掉已显示的(后两类仅 Mac/Linux),RemoveNotification 仅 Linux 专用。
NotemacOS 和 Windows 上
RemoveNotification是个空实现,永远返回 nil。跨平台代码里调它不会报错,但只有 Linux 真正生效,别指望它能全局清通知。
18-7 一个完整流程示例
把前面几步串起来,做一个「收到消息就发可交互通知」的绑定方法。前端收到事件后根据 taskId 跳转详情页:
// 初始化时注册一次分类
func (a *App) startup(ctx context.Context) {
a.ctx = ctx
runtime.InitializeNotifications(ctx)
runtime.RegisterNotificationCategory(ctx, runtime.NotificationCategory{
ID: "msg-category",
Actions: []runtime.NotificationAction{{ID: "OPEN", Title: "打开"}},
})
// 统一接收响应并转事件
runtime.OnNotificationResponse(ctx, func(r runtime.NotificationResult) {
if r.Error == nil {
runtime.EventsEmit(ctx, "notification-response", r.Response)
}
})
}
// 业务侧触发通知,带上业务数据
func (a *App) NotifyNewMessage(from, text, taskID string) error {
return runtime.SendNotificationWithActions(a.ctx, runtime.NotificationOptions{
ID: "msg-" + taskID,
Title: "来自 " + from,
Body: text,
CategoryID: "msg-category",
Data: map[string]interface{}{"taskId": taskID},
})
}
响应里取回自定义数据靠 resp.UserInfo,它是 map[string]interface{},取值后要做类型断言:
runtime.OnNotificationResponse(a.ctx, func(r runtime.NotificationResult) {
if r.Error != nil {
return
}
if id, ok := r.Response.UserInfo["taskId"].(string); ok {
runtime.LogInfo(a.ctx, "点击了任务: "+id)
}
})
这种「注册一次 + 绑定触发 + 事件转发 + userInfo 取数」的骨架,可以套到绝大多数通知场景里。
常见误区
没初始化就发。漏掉 InitializeNotifications 会发送失败,日志里才看得到。
把关键信息放 Subtitle。Windows 不显示副标题,用户看不到。
在前端等 OnNotificationResponse。JS 运行时没有这个函数,必须用 Go 回调 + Events 转发。
忽略 macOS 授权一次性。用户拒绝后应用内无法再次弹系统框,只能引导去系统设置。
忘记 CleanupNotifications。Linux 下不清理会残留 D-Bus 连接,长期运行的应用尤其要注意。
以为 TEXT_REPLY 全平台都有。Linux 不支持文本输入,回复框不会出现。
小结
Notification 模块用 InitializeNotifications 先初始化,IsNotificationAvailable 确认平台,macOS 还要走 Check/RequestNotificationAuthorization 授权。
基础通知用 SendNotification,交互通知先 RegisterNotificationCategory 再 SendNotificationWithActions,靠 HasReplyField 加回复框。用户点击只能在 Go 侧用 OnNotificationResponse 接收,再 EventsEmit 转给前端。
三大平台能力有差异,Windows 无副标题、Linux 无文本输入、macOS 要授权。退出时 CleanupNotifications 收尾。到此 Runtime 的原生能力就讲完了,下一章进入窗口 API。