首页 / Wails 入门教程 / 系统通知 Notifications

Wails 入门教程

系统通知 Notifications

本教程共 42 篇 · 第 18 篇 · 更新于 2026-08-03

Wails桌面开发Notification通知交互通知

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,写上去也无害。

Warning

macOS 的权限弹窗只有一次机会。用户点了「不允许」之后,应用内再调用 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())
}

NotificationActionDestructive 字段只在 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 专用。

Note

macOS 和 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,交互通知先 RegisterNotificationCategorySendNotificationWithActions,靠 HasReplyField 加回复框。用户点击只能在 Go 侧用 OnNotificationResponse 接收,再 EventsEmit 转给前端。

三大平台能力有差异,Windows 无副标题、Linux 无文本输入、macOS 要授权。退出时 CleanupNotifications 收尾。到此 Runtime 的原生能力就讲完了,下一章进入窗口 API。