Web Push 的核心在于它绕过了页面必须打开的限制,借助 Service Worker 与浏览器推送服务建立长连接通道。当 PHP 后端需要发送通知时,它并不是直接连接用户浏览器,而是将加密后的消息提交给浏览器厂商的推送服务(如 Chrome 使用的 FCM、Firefox 使用的 Mozilla autopush),由推送服务负责最终投递。这意味着后端只需掌握一个订阅端点(subscription endpoint)和对应的密钥,就能完成推送。整个链路涉及 VAPID 身份验证、消息加密、Service Worker 事件监听等多个环节,下面逐一拆解。

Web Push 的技术基础:为什么需要 Service Worker 和 VAPID
Service Worker 是运行在浏览器后台的独立线程,即使页面关闭也能存活。它监听 push 事件,当推送服务把消息送达浏览器时,Service Worker 负责展示系统通知。没有 Service Worker,浏览器无法在页面不活跃时接收推送。因此,前端必须注册一个 Service Worker 脚本,并在其中监听 push 事件。
但仅仅有 Service Worker 还不够,服务器需要向推送服务证明自己的身份。VAPID(Voluntary Application Server Identification)就是为此设计的。它使用非对称加密:服务器生成一对公钥和私钥,公钥交给前端用于生成订阅,私钥保留在后端用于对推送请求签名。推送服务会验证签名,从而确认请求来自合法的应用服务器,也便于统计和限制滥用。
另外,推送消息在传输过程中必须加密,否则推送服务可以读取明文内容。加密依赖订阅对象中的 p256dh 和 auth 密钥,使用椭圆曲线 Diffie-Hellman(ECDH)算法协商出共享密钥,再用 AES-GCM 加密载荷。这些细节通常由成熟的库处理,但理解原理有助于排查问题。
前端实现:获取权限并生成订阅对象
前端的任务是注册 Service Worker、请求通知权限,并调用 PushManager.subscribe 生成订阅对象。订阅对象包含 endpoint 和 keys,必须发送到后端保存。下面是完整的 JavaScript 示例:
// 注册 Service Worker
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js').then(function(registration) {
console.log('Service Worker 注册成功', registration);
}).catch(function(err) {
console.error('Service Worker 注册失败', err);
});
}
// 请求通知权限
Notification.requestPermission().then(function(permission) {
if (permission === 'granted') {
// 获取订阅对象
navigator.serviceWorker.ready.then(function(registration) {
const applicationServerKey = urlBase64ToUint8Array('你的公钥');
registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: applicationServerKey
}).then(function(subscription) {
// 将 subscription 发送到后端保存
fetch('/save-subscription', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(subscription)
});
});
});
}
});
// 辅助函数:将 base64url 公钥转为 Uint8Array
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;
}
订阅对象是一个 JSON,典型结构如下:
{
"endpoint": "https://fcm.googleapis.com/fcm/send/xxxxx",
"expirationTime": null,
"keys": {
"p256dh": "BNcRd...",
"auth": "xxxxx"
}
}
其中 endpoint 是推送服务的 URL,后端发送请求时直接 POST 到这个地址。p256dh 是客户端公钥,auth 是认证密钥,它们用于加密消息。需要注意的是,不同浏览器的订阅对象可能略有差异,但核心字段一致。一旦用户清空浏览器数据或卸载应用,订阅可能失效,因此后端必须处理推送失败并清理无效订阅。
权限请求要注意时机:应在用户有明确操作(如点击“开启通知”按钮)时请求,而不是页面加载时立即弹窗,否则容易被浏览器拦截或引起用户反感。如果用户之前拒绝过权限,再次调用 requestPermission 可能不会弹出对话框,需要引导用户到浏览器设置中手动开启。
PHP 后端发送推送:使用 php-web-push 库的完整流程
PHP 生态中最常用的 Web Push 库是 minishlink/web-push,它封装了 VAPID 签名、消息加密和 HTTP 请求。首先通过 Composer 安装:
composer require minishlink/web-push
安装完成后,在 PHP 脚本中创建 WebPush 实例并设置 VAPID 认证信息。公钥和私钥可以通过库提供的命令生成,或者使用在线工具。生产环境中建议将私钥存放在安全位置,不要硬编码在代码中。下面是一个完整的发送示例:
<?php
require 'vendor/autoload.php';
use Minishlink\WebPush\WebPush;
use Minishlink\WebPush\Subscription;
// VAPID 密钥对(实际应从配置或环境变量读取)
$publicKey = '你的公钥';
$privateKey = '你的私钥';
$subject = 'mailto:admin@ipipp.com'; // 联系邮箱,推送服务可能要求
$auth = [
'VAPID' => [
'subject' => $subject,
'publicKey' => $publicKey,
'privateKey' => $privateKey,
],
];
$webPush = new WebPush($auth);
// 假设从数据库获取到的订阅 JSON
$subscriptionJson = '{"endpoint":"https://fcm.googleapis.com/fcm/send/xxxxx","keys":{"p256dh":"BNcRd...","auth":"xxxxx"}}';
$subscription = Subscription::create(json_decode($subscriptionJson, true));
// 通知内容
$payload = json_encode([
'title' => '新消息',
'body' => '您有一条新的通知',
'icon' => '/icon.png',
'url' => '/detail/123'
]);
// 发送推送
$result = $webPush->sendOneNotification(
$subscription,
$payload
);
// 检查结果
if ($result->isSuccess()) {
echo '推送成功';
} else {
$reason = $result->getReason();
echo '推送失败,原因:' . $reason;
// 如果是 410 Gone,表示订阅已失效,应删除该订阅
if ($result->getStatusCode() === 410) {
// 从数据库删除 $subscriptionJson 对应的记录
}
}
sendOneNotification 方法会完成消息加密、构建 Web Push 请求并发送到订阅端点。payload 必须是字符串,通常使用 JSON 格式,因为 Service Worker 收到后可以自定义展示。如果推送服务返回成功状态码(如 201),表示消息已被接受,但并不意味着用户一定看到了通知(例如用户关闭了系统通知)。
实际应用中,你可能需要批量发送。可以使用 sendNotifications 方法传入多个订阅,但注意不要一次性发送过多,避免触发推送服务的速率限制。对于失败的订阅,应根据状态码(特别是 404 和 410)及时清理,防止反复发送无效请求。
原生实现与常见问题排查
如果不想引入第三方库,也可以手动实现 Web Push 协议。核心步骤包括:生成 VAPID 密钥对、使用私钥对 JWT 签名(包含 aud、exp、sub 等声明)、根据 RFC 8291 进行消息加密(需要实现 ECDH 密钥协商和 AES-128-GCM 加密),然后向 endpoint 发送 POST 请求,头部包含 Authorization 和 Crypto-Key 等字段。虽然可行,但实现复杂且容易出错,因此绝大多数项目都应该优先使用成熟库。
开发过程中常见的问题有:本地开发时通知不显示?首先要确认浏览器是否支持 Notification API 和 Service Worker,并检查是否通过 HTTPS 访问(localhost 被视为安全上下文,可以使用)。其次检查 Service Worker 是否成功注册,以及在 push 事件中是否正确调用了 event.waitUntil 和 showNotification。Chrome 的开发者工具中 Application 面板可以查看 Service Worker 状态和推送订阅信息。
另一个高频问题是订阅失效。用户清除浏览器缓存、卸载 PWA 应用或推送服务端重置密钥都可能导致订阅无效。后端在发送推送后必须检查响应状态码,如果是 404 或 410,应立即从数据库中删除该订阅。此外,推送消息的 payload 大小有限制,通常为 4KB 左右,超限会被推送服务拒绝。对于需要大文本的通知,应使用标题和摘要引导用户点击后获取完整内容。
最后,Web Push 是提升用户留存的有效手段,但也要注意推送频率和内容相关性,过度推送会导致用户直接关闭通知权限。在 PHP 后端接入 Web Push 并不复杂,关键是要理解整个链条,并在前端、后端和 Service Worker 三个环节做好配合。