Реализация системных уведомлений в десктоп-приложении
Системные уведомления — один из немногих каналов, через который десктоп-приложение общается с пользователем даже когда окно свёрнуто. На практике реализация упирается в три разных API в зависимости от стека: Electron (Node.js Notification + нативный модуль), Tauri (tauri-plugin-notification), или браузерный Notification API для PWA. Каждый путь со своими ограничениями — рассмотрим все три.
Electron: нативные уведомления
В Electron за уведомления отвечает класс Notification из electron package — он оборачивает нативные механизмы каждой ОС (WinRT Toast на Windows 10/11, NSUserNotificationCenter / UNUserNotificationCenter на macOS, libnotify на Linux).
// main/notifications.ts
import { Notification, nativeImage, app } from 'electron'
import path from 'path'
export interface NotificationOptions {
title: string
body: string
icon?: string
urgency?: 'normal' | 'critical' | 'low'
actions?: Array<{ type: 'button'; text: string }>
timeoutType?: 'default' | 'never'
toastXml?: string // Windows-only: raw Toast XML
}
export function sendNotification(opts: NotificationOptions): Notification {
const iconPath = opts.icon
? nativeImage.createFromPath(path.resolve(opts.icon))
: nativeImage.createFromPath(
path.join(app.getAppPath(), 'resources', 'icon.png')
)
const n = new Notification({
title: opts.title,
body: opts.body,
icon: iconPath,
urgency: opts.urgency ?? 'normal',
timeoutType: opts.timeoutType ?? 'default',
actions: opts.actions,
toastXml: opts.toastXml,
})
n.on('click', () => {
// восстановить/сфокусировать главное окно
const { BrowserWindow } = require('electron')
const win = BrowserWindow.getAllWindows()[0]
if (win) {
if (win.isMinimized()) win.restore()
win.focus()
}
})
n.on('action', (_event, index) => {
console.log(`Action clicked: ${index}`)
})
n.show()
return n
}
На Windows 10+ уведомления отображаются в Action Center. Чтобы они там появлялись корректно, приложению нужен Application User Model ID (AppUserModelID). Electron устанавливает его автоматически через app.setAppUserModelId():
// main/index.ts
import { app } from 'electron'
app.setAppUserModelId('com.yourcompany.yourapp')
// Должно быть вызвано до первого show() уведомления
Без этого вызова уведомления на Windows могут не группироваться и пропадать из центра уведомлений после перезапуска.
IPC: отправка уведомлений из renderer
Renderer-процесс не имеет прямого доступа к Notification из electron — только main process. Правильная схема: renderer -> ipcRenderer.invoke -> main -> Notification.
// preload/index.ts
import { contextBridge, ipcRenderer } from 'electron'
contextBridge.exposeInMainWorld('notifications', {
send: (opts: NotificationOptions) =>
ipcRenderer.invoke('notification:send', opts),
})
// main/ipc-handlers.ts
import { ipcMain } from 'electron'
import { sendNotification } from './notifications'
ipcMain.handle('notification:send', (_event, opts) => {
sendNotification(opts)
})
// renderer/src/hooks/useNotification.ts
declare global {
interface Window {
notifications: {
send: (opts: NotificationOptions) => Promise<void>
}
}
}
export function useNotification() {
const send = (opts: NotificationOptions) => {
if (window.notifications) {
window.notifications.send(opts)
} else {
// fallback для dev-режима в браузере
new Notification(opts.title, { body: opts.body })
}
}
return { send }
}
Tauri: плагин уведомлений
В Tauri 2.x уведомления реализуются через tauri-plugin-notification. Добавление:
# src-tauri/Cargo.toml
[dependencies]
tauri-plugin-notification = "2"
// src-tauri/src/main.rs (регистрация плагина)
tauri::Builder::default()
.plugin(tauri_plugin_notification::init())
.run(tauri::generate_context!())
.expect("error while running tauri application");
// src/lib/notifications.ts
import {
isPermissionGranted,
requestPermission,
sendNotification,
} from '@tauri-apps/plugin-notification'
export async function notify(title: string, body: string) {
let permissionGranted = await isPermissionGranted()
if (!permissionGranted) {
const permission = await requestPermission()
permissionGranted = permission === 'granted'
}
if (permissionGranted) {
sendNotification({ title, body })
}
}
На macOS Tauri-приложения требуют подписи (code signing) для показа уведомлений в production. В dev-режиме уведомления работают через браузерный API.
Браузерный Notification API (PWA / Electron renderer fallback)
Для PWA и как запасной вариант в renderer без IPC:
// src/services/notification.service.ts
export class NotificationService {
private static permission: NotificationPermission = 'default'
static async requestPermission(): Promise<boolean> {
if (!('Notification' in window)) return false
if (Notification.permission === 'granted') return true
if (Notification.permission === 'denied') return false
const result = await Notification.requestPermission()
this.permission = result
return result === 'granted'
}
static async send(
title: string,
options: NotificationOptions & {
onClick?: () => void
} = {}
): Promise<Notification | null> {
const granted = await this.requestPermission()
if (!granted) return null
const { onClick, ...nativeOptions } = options
const notification = new Notification(title, nativeOptions)
if (onClick) {
notification.onclick = () => {
window.focus()
onClick()
}
}
return notification
}
}
В Service Worker для PWA уведомления отправляются через self.registration.showNotification() — это единственный способ показать их, когда вкладка закрыта:
// service-worker.js
self.addEventListener('push', (event) => {
const data = event.data?.json() ?? {}
event.waitUntil(
self.registration.showNotification(data.title ?? 'Уведомление', {
body: data.body,
icon: '/icons/icon-192x192.png',
badge: '/icons/badge-72x72.png',
data: { url: data.url },
actions: data.actions ?? [],
})
)
})
self.addEventListener('notificationclick', (event) => {
event.notification.close()
const url = event.notification.data?.url ?? '/'
event.waitUntil(clients.openWindow(url))
})
Очередь и дедупликация
В сложных приложениях уведомления нужно ставить в очередь, чтобы не завалить пользователя. Простая реализация с throttle:
// src/services/notification-queue.ts
interface QueuedNotification {
id: string
title: string
body: string
timestamp: number
}
export class NotificationQueue {
private queue: QueuedNotification[] = []
private shown = new Set<string>()
private timer: ReturnType<typeof setTimeout> | null = null
private readonly cooldownMs: number
constructor(cooldownMs = 3000) {
this.cooldownMs = cooldownMs
}
enqueue(id: string, title: string, body: string) {
if (this.shown.has(id)) return // дедупликация по ID
this.queue.push({ id, title, body, timestamp: Date.now() })
this.process()
}
private process() {
if (this.timer) return
const item = this.queue.shift()
if (!item) return
this.shown.add(item.id)
new Notification(item.title, { body: item.body })
// через 10 сек ID снова доступен
setTimeout(() => this.shown.delete(item.id), 10_000)
this.timer = setTimeout(() => {
this.timer = null
this.process()
}, this.cooldownMs)
}
}
Типичные сроки
Базовая интеграция уведомлений через Electron IPC с обработчиком click — от 4 часов. Полная реализация с очередью, дедупликацией, кастомными Toast XML для Windows, поддержкой action-кнопок и тестами — 1–2 рабочих дня.







