Maui Blazor 中文社区 QQ群:645660665

企业H5站点升级PWA (九)

如何在我的 Service Worker 中实现推送通知?

下面给你一套最小可用的 Web Push 集成思路,结合你现有的 sw.jsindex.html,分成两块:

  1. 前端页面:注册推送、订阅并把订阅信息发给服务器。
  2. Service Worker:处理 push 事件并弹通知。

注意:真正“推送”是服务器发到浏览器的,你本地只能模拟(DevTools 里的“推送”按钮就是在触发 push 事件)。


1. 在 sw.js 里处理 push 通知

先在你现有的 sw.js 末尾增加 push 和通知点击事件处理:

self.addEventListener('push', (event) => {
    // 约定服务器发送的数据为 JSON 字符串
    let data = {};
    if (event.data) {
        try {
            data = event.data.json();
        } catch {
            data = { body: event.data.text() };
        }
    }

    const title = data.title || 'DensenApp 通知';
    const options = {
        body: data.body || '您有一条新消息。',
        icon: data.icon || './densen26.jpg',
        badge: data.badge || './densen26.jpg',
        data: {
            url: data.url || './index.html', // 点击通知要打开的地址
            ...data
        }
    };

    event.waitUntil(
        self.registration.showNotification(title, options)
    );
});

self.addEventListener('notificationclick', (event) => {
    event.notification.close();
    const url = event.notification.data?.url || './index.html';

    event.waitUntil(
        self.clients.matchAll({ type: 'window', includeUncontrolled: true })
            .then((clientList) => {
                // 如果已有同源窗口,直接聚焦并导航
                for (const client of clientList) {
                    if ('focus' in client) {
                        client.focus();
                        if (url && 'navigate' in client) {
                            client.navigate(url);
                        }
                        return;
                    }
                }
                // 否则新开一个窗口
                if (self.clients.openWindow && url) {
                    return self.clients.openWindow(url);
                }
                return null;
            })
    );
});

这样,收到 push 时就会弹系统通知;点击通知会打开(或切换到)你的页面。


2. 在页面端发起推送订阅

2.1 准备 VAPID 公钥

Web Push 需要 VAPID 公私钥对,一般在服务器(.NET)生成:

  • 可以用 ASP.NET Core / 控制台配合 WebPush 库生成;
  • 这里只假定你已经拿到一个 Base64 URL 安全格式的公钥字符串,例如:
BPx...你的VAPID公钥...abc

下面代码里用 const VAPID_PUBLIC_KEY = '...'; 占位。

2.2 在 index.html / 某个 JS 里增加订阅逻辑

建议在你当前 index.html 里已有的 Service Worker 注册逻辑旁边,增加一个“初始化推送”的调用。

把原来的 <script> 末尾稍微整理一下:

    <script src="bootstrap.bundle.min.js"></script>
    <script src="./js/index.js"></script>
    <script>
        const VAPID_PUBLIC_KEY = '在这里填你的VAPID公钥';

        async function initServiceWorkerAndPush() {
            if (!('serviceWorker' in navigator)) {
                return;
            }

            // 注册 SW
            const registration = await navigator.serviceWorker.register('./sw.js');
            console.log('Service Worker 注册成功:', registration.scope);

            // 监听 SW 激活刷新
            navigator.serviceWorker.addEventListener('message', (event) => {
                if (event.data?.type === 'SW_ACTIVATED') {
                    console.log('Service Worker 已更新到版本:', event.data.version);
                    window.location.reload();
                }
            });

            // 申请通知权限
            const permission = await Notification.requestPermission();
            if (permission !== 'granted') {
                console.warn('用户拒绝通知权限');
                return;
            }

            // 订阅 push
            const subscription = await registration.pushManager.subscribe({
                userVisibleOnly: true,
                applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY)
            });

            console.log('Push 订阅成功:', subscription);

            // TODO: 把 subscription 发送到你的服务器保存起来
            // 例如:
            // await fetch(API_BASE + '/api/push/subscribe', {
            //     method: 'POST',
            //     headers: { 'Content-Type': 'application/json' },
            //     body: JSON.stringify(subscription)
            // });
        }

        function urlBase64ToUint8Array(base64String) {
            const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
            const base64 = (base64String + padding)
                .replace(/-/g, '+')
                .replace(/_/g, '/');

            const rawData = window.atob(base64);
            const outputArray = new Uint8Array(rawData.length);

            for (let i = 0; i < rawData.length; ++i) {
                outputArray[i] = rawData.charCodeAt(i);
            }
            return outputArray;
        }

        window.addEventListener('load', () => {
            initServiceWorkerAndPush().catch(err => {
                console.error('初始化 Service Worker / Push 失败:', err);
            });
        });
    </script>
</body>
</html>

要点:

  • Notification.requestPermission() 会弹系统权限对话框;
  • registration.pushManager.subscribe(...) 创建订阅;
  • subscription 需要发给你的服务器保存(包含 endpoint + 密钥)。

3. 服务器端发送 Web Push(简要)

你后端是 .NET,可以用 WebPush.Net 之类的库向浏览器推送:

using WebPush;

var subject = "mailto:[email protected]";
var publicKey = "<与你前端相同的VAPID公钥>";
var privateKey = "<VAPID私钥>";

var client = new WebPushClient();
var vapidDetails = new VapidDetails(subject, publicKey, privateKey);

// subscriptionJson 是你从前端保存的 Subscription 对象 JSON
var subscription = JsonSerializer.Deserialize<PushSubscription>(subscriptionJson);

var payload = new
{
    title = "DensenApp 通知",
    body = "有新的订单,请及时处理",
    url = "https://你的站点地址/orders.html"
};

await client.SendNotificationAsync(
    subscription,
    JsonSerializer.Serialize(payload),
    vapidDetails);

这条消息就会触发前面 sw.js 里的 push 事件,然后弹通知。


4. 开发时如何快速验证

  1. 本地调试

    • 必须在 https://http://localhost 下访问;
    • 允许浏览器通知;
    • 看 console 中是否有“Push 订阅成功”。
  2. 用 DevTools 的“推送”按钮模拟

    • 在 Application → Service Workers 面板;
    • 输入如 {"title":"测试","body":"来自DevTools的推送"}
    • 点击“推送”,应看到通知弹出。

如果你希望,我可以再帮你把服务器端 .NET 发送推送那块按你现在的 WebAPI 结构(看看 WebApp.cs)嵌一段实际的控制器示例。

posted @ 2026-02-18 20:14  AlexChow  阅读(1675)  评论(0)    收藏  举报