如何用PHP项目实现Web Push?

wen java案例 2

如何用PHP项目实现Web Push?从零搭建浏览器推送通知系统

目录导读

  1. Web Push 技术原理与适用场景
  2. PHP 实现 Web Push 的核心组件
  3. 服务端准备工作:生成 VAPID 密钥对
  4. 客户端:注册 Service Worker 与订阅推送
  5. 服务端:使用 PHP 发送推送通知
  6. 数据库存储与用户管理策略
  7. 安全性考量与常见问题避坑
  8. FAQ:开发者最关心的 5 个问题

Web Push 技术原理与适用场景

Web Push(浏览器推送通知)允许网站在用户未打开网页时,仍能通过操作系统向用户推送消息,这项技术基于 Push API (客户端) 和 Web Push Protocol (服务端) 实现,核心工作流如下:

如何用PHP项目实现Web Push?

  • 用户访问网站 → 同意接收通知 → 浏览器生成 PushSubscription 对象(包含推送端点、公钥和身份令牌)
  • 服务端将 PushSubscription 保存到数据库
  • 当需要推送时,服务端通过 HTTP POST 请求将加密消息发送到推送端点(通常由浏览器厂商提供,如 FCM、APNs)
  • 浏览器解密消息后,触发 Service Worker 显示通知

适用场景:电商促销提醒、博客更新通知、即时通讯消息提醒、SaaS 服务状态变更通知等。


PHP 实现 Web Push 的核心组件

要用 PHP 实现完整的 Web Push,你需要以下组件:

组件 说明
VAPID 密钥 用于服务端身份验证,需生成一对公钥和私钥
Service Worker JavaScript 文件,负责接收推送事件并显示通知
客户端 JS 注册 Service Worker、创建订阅、发送订阅数据到服务器
PHP 服务端 保存订阅、调用 Web Push 库发送通知
数据库 存储用户的 PushSubscription 对象

推荐 PHP 库minishlink/web-push(PHP 7.4+ 兼容,Github 2.5k+ Stars),它封装了加密、VAPID 签名和 HTTP 请求。


服务端准备工作:生成 VAPID 密钥对

VAPID (Voluntary Application Server Identification) 是 Web Push 的强制要求,在 PHP 项目根目录执行:

composer require minishlink/web-push

然后通过 PHP 代码生成密钥对:

require 'vendor/autoload.php';
use Minishlink\WebPush\VAPID;
$keys = VAPID::createVapidKeys();
echo "Public Key: " . $keys['publicKey'] . "\n";
echo "Private Key: " . $keys['privateKey'] . "\n";

将生成的公钥和私钥保存到环境变量或配置文件中(如 .env),切勿硬编码在源码中。


客户端:注册 Service Worker 与订阅推送

Step 1:创建 sw.js (Service Worker 文件)

self.addEventListener('push', event => {
  const data = event.data.json();
  const options = {
    body: data.body,
    icon: '/icon.png',
    badge: '/badge.png'
  };
  event.waitUntil(
    self.registration.showNotification(data.title, options)
  );
});

Step 2:客户端 JS 请求权限并订阅

if ('Notification' in window && 'serviceWorker' in navigator) {
  Notification.requestPermission().then(permission => {
    if (permission === 'granted') {
      navigator.serviceWorker.register('/sw.js').then(registration => {
        // 使用 VAPID 公钥创建订阅
        const applicationServerKey = urlBase64ToUint8Array('你的VAPID公钥');
        registration.pushManager.subscribe({
          userVisibleOnly: true,
          applicationServerKey: applicationServerKey
        }).then(subscription => {
          // 将 subscription 对象发送到 PHP 后端保存
          fetch('/save-subscription.php', {
            method: 'POST',
            body: JSON.stringify(subscription),
            headers: { 'Content-Type': 'application/json' }
          });
        });
      });
    }
  });
}

注意applicationServerKey 需要从 Base64 转换为 Uint8Array(参考 MDN 提供的转换函数)。


服务端:使用 PHP 发送推送通知

保存订阅的 PHP 端点(save-subscription.php

<?php
require 'vendor/autoload.php';
$input = json_decode(file_get_contents('php://input'), true);
$subscription = [
    'endpoint' => $input['endpoint'],
    'keys' => [
        'p256dh' => $input['keys']['p256dh'],
        'auth' => $input['keys']['auth']
    ]
];
// 保存到数据库(使用 PDO 示例)
$db = new PDO('sqlite:subscriptions.db');
$stmt = $db->prepare('INSERT INTO subscriptions (endpoint, p256dh, auth) VALUES (?, ?, ?)');
$stmt->execute([$subscription['endpoint'], $subscription['keys']['p256dh'], $subscription['keys']['auth']]);
echo 'ok';

发送推送通知(send-push.php

<?php
require 'vendor/autoload.php';
use Minishlink\WebPush\WebPush;
use Minishlink\WebPush\Subscription;
$auth = [
    'VAPID' => [
        'subject' => 'mailto:your-email@example.com', // 必须为邮箱格式
        'publicKey' => '你的公钥',
        'privateKey' => '你的私钥'
    ]
];
$webPush = new WebPush($auth);
// 从数据库读取所有订阅
$db = new PDO('sqlite:subscriptions.db');
$rows = $db->query('SELECT * FROM subscriptions')->fetchAll(PDO::FETCH_ASSOC);
foreach ($rows as $row) {
    $subscription = Subscription::create([
        'endpoint' => $row['endpoint'],
        'publicKey' => $row['p256dh'],
        'authToken' => $row['auth'],
    ]);
    $payload = json_encode([
        'title' => '新消息通知',
        'body' => '您有一条新的订单状态更新'
    ]);
    $webPush->queueNotification($subscription, $payload);
}
// 发送所有队列中的通知
foreach ($webPush->flush() as $report) {
    if (!$report->isSuccess()) {
        // 处理失败:如果端点返回 410 Gone,删除该订阅
        error_log('推送失败: ' . $report->getEndpoint());
    }
}

数据库存储与用户管理策略

推荐表结构(MySQL/MariaDB 示例):

CREATE TABLE push_subscriptions (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_id INT,                -- 关联你的用户系统
    endpoint TEXT NOT NULL,
    p256dh VARCHAR(255) NOT NULL,
    auth VARCHAR(255) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY unique_endpoint (endpoint(255))
);

优化建议

  • 设置 endpoint 唯一索引,避免重复订阅
  • 定期清理失效订阅(推送失败时删除对应记录)
  • 关联用户 ID 实现定向推送

安全性考量与常见问题避坑

  1. HTTPS 强制要求:Web Push 只能在 HTTPS 或 localhost 环境下工作
  2. 权限管理:始终检查 Notification.permission,不要擅自弹出请求
  3. 消息加密minishlink/web-push 库自动处理加密,无需手动实现
  4. 错误处理:推送失败时检查响应状态码:
    • 410 Gone:订阅已过期,立即删除该记录
    • 429 Too Many Requests:降级推送频率
  5. 测试环境:Chrome DevTools → Application → Service Workers 中手动触发推送测试

FAQ:开发者最关心的 5 个问题

Q1:为什么我的推送通知只有 Chrome 能收到,Safari 收不到?

A:Safari 14 以后开始支持 Web Push,但要求:

  • 使用 macOS 或 iOS 16.4+
  • 必须先将网站添加到主屏幕(PWA)
  • 需要使用 Apple 的推送证书(非 VAPID),建议查阅苹果开发者文档

Q2:用户关闭浏览器后推送还能收到吗?

A:只要操作系统推送服务(如 FCM/APNs)可达,即使浏览器完全关闭也能收到,但如果用户清除了网站数据或禁用了通知权限,则无法接收。

Q3:推送通知有大小限制吗?

A:推荐 < 4KB,实际限制由浏览器决定(Chrome 约 4KB, Firefox 约 64KB),大负载请使用推送通知中的 data 字段传递 ID,客户端通过 API 获取完整内容。

Q4:如何实现“只向特定用户”推送?

A:在保存订阅时关联用户 ID,发送时根据业务逻辑查询符合条件的用户订阅列表,循环发送。

Q5:免费方案能支撑多少用户?

A:Web Push 本身不产生直接成本(通过浏览器厂商的推送端点发送),但大量推送可能触发服务商的速率限制,对于小型项目(< 1 万订阅),免费方案通常够用。


PHP 实现 Web Push 的路径清晰:VAPID 密钥生成 → Service Worker 注册 → 订阅保存 → 后端推送,关键点在于正确处理订阅的生命周期(添加、失效、删除)和加密通信,对于大型生产环境,建议添加队列系统(如 Redis)异步处理推送,避免 PHP 进程阻塞。

扩展阅读

  • MDN:Push API 文档
  • GitHub:minishlink/web-push 项目 Wiki
  • Google Developers:Web Push 最佳实践

注意:推送通知对用户体验影响大,请确保只在必要时推送有价值内容,避免过度打扰用户。

抱歉,评论功能暂时关闭!