前置硬性条件:站点必须部署 HTTPS,http 协议环境无法启用 WebPush,本地 localhost 调试不受该限制。
整体实现流程

整套网页推送一共涉及五个部分:前端页面、Service‑Worker、VAPID密钥、PHP服务端、厂商推送网关。
- 用户访问HTTPS网站,浏览器申请通知权限,生成订阅对象 Subscription。
- 前端把订阅信息
endpoint、auth、p256dh提交给后端,持久化存入数据库。 - PHP后端使用VAPID密钥加密推送内容,通过POST请求发送到浏览器对应的厂商推送网关:
- iPhone Safari PWA:转发到 Apple APNs 苹果推送服务
- 安卓端Chrome:转发到 Google FCM 谷歌推送服务
- 厂商网关完成消息中转,下发到用户手机设备。
- 设备接收消息分两种场景:
- 锁屏、应用后台挂起:由系统直接弹出通知,不会执行 JS 代码。
- 网页处于前台打开:触发 Service‑Worker 的 push 事件,可以执行自定义 JS 逻辑,比如播放提示音。
重要逻辑:服务端不会直接访问用户手机,所有消息都经过苹果、谷歌的服务中转。
一、生成VAPID密钥
一个域名对应一套独立密钥,禁止多个站点共用同一套密钥。
通过composer安装web‑push依赖,执行命令生成密钥对:
composer require minishlink/web-push
./vendor/bin/web-push generate-vapid-keys
执行后输出公私钥:
Public Key: BLabxxxx
Private Key: xxxxx
- 公钥:交给前端JS使用
- 私钥:保存在PHP后端,绝对不能对外泄露
二、配置 manifest.json(iOS PWA 必备)
iPhone 将网页添加到桌面PWA,强依赖这个配置文件。通知栏上 from XXX 的来源文字,读取的就是文件里的 name / short_name。
注意:修改名称之后,iPhone 需要删掉旧的桌面PWA图标,重新添加到主屏幕,并且重新授权订阅通知,改动才会生效。
manifest.json 示例:
{
"name": "消息助手",
"short_name": "消息助手",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#2563eb",
"icons": [
{
"src": "/icon-192.png",
"sizes": "192x192",
"type": "image/png"
}
]
}
在网页 HTML 的 head 标签引入配置:
<link rel="manifest" href="/manifest.json">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-title" content="消息助手">
三、前端订阅页面 index.html
页面提供按钮,用户点击后完成 ServiceWorker 注册、通知权限申请、订阅,再将订阅信息提交后端保存。
<!DOCTYPE html>
<html lang="zh‑CN">
<head>
<meta charset="UTF‑8">
<link rel="manifest" href="/manifest.json">
<meta name="apple‑mobile‑web‑app‑capable" content="yes">
<meta name="apple‑mobile‑web‑app‑title" content="消息助手">
</head>
<body>
<button id="subBtn">开启消息通知</button>
<script>
// 替换为自己的VAPID公钥
const VAPID_PUBLIC_KEY = "你的VAPID公钥";
// base64格式转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);
return Uint8Array.from([...rawData].map(char => char.charCodeAt(0)));
}
async function subscribePush() {
if(!('serviceWorker' in navigator) || !('PushManager' in window)){
alert("你的浏览器不支持WebPush");
return;
}
// 注册service‑worker
const swReg = await navigator.serviceWorker.register('/service-worker.js');
await swReg.ready;
// 创建推送订阅
const subscription = await swReg.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY)
});
// 将订阅数据POST提交后端存储
await fetch('/save-sub.php',{
method:"POST",
headers:{"Content‑Type":"application/json"},
body:JSON.stringify(subscription)
});
alert("订阅成功!可以接收推送");
}
document.querySelector("#subBtn").addEventListener("click",subscribePush);
</script>
</body>
</html>
四、service‑worker.js 监听推送事件
文件必须放在网站根目录,否则 ServiceWorker 作用域失效,推送无法正常触发。
// 监听推送消息
self.addEventListener('push', (event) => {
let payload = {};
if(event.data){
payload = event.data.json();
}
const title = payload.title || "新消息通知";
const options = {
body: payload.body || "您收到一条新消息",
icon: "/icon-192.png"
};
event.waitUntil(self.registration.showNotification(title, options));
});
// 点击通知跳转页面
self.addEventListener('notificationclick', (event) => {
event.notification.close();
event.waitUntil(clients.openWindow('/'));
});
五、PHP后端:接收并保存订阅 save-sub.php
接收前端上传的 subscription 数据,实际业务中把 endpoint、auth、p256dh 存入数据库,示例仅做输出演示。
<?php
$raw = file_get_contents('php://input');
$subscription = json_decode($raw,true);
// 业务逻辑:把subscription里面 endpoint、keys[auth]、keys[p256dh]写入数据库
echo json_encode([
"code"=>0,
"msg"=>"保存订阅成功",
"data"=>$subscription
]);
六、PHP发送推送 send-push.php
依赖 minishlink/web‑push 库,读取数据库中的订阅信息,构建消息并发起推送。
<?php
require __DIR__.'/vendor/autoload.php';
use Minishlink\WebPush\WebPush;
use Minishlink\WebPush\Subscription;
$vapid = [
'publicKey' => "你的VAPID公钥",
'privateKey' => "你的VAPID私钥",
];
$auth = [
'VAPID' => $vapid
];
$webPush = new WebPush($auth);
// 从数据库查询获取的单条订阅记录
$subData = [
"endpoint"=>"xxx",
"keys"=>[
"auth"=>"xxx",
"p256dh"=>"xxx"
]
];
$subscription = Subscription::create($subData);
// 推送消息体
$payload = json_encode([
"title" => "新消息通知",
"body" => "您收到一条新消息"
]);
$webPush->queueNotification($subscription,$payload);
// 批量执行推送,打印结果
foreach ($webPush->flush() as $report) {
$endpoint = $report->getEndpoint();
if ($report->isSuccess()) {
echo "推送成功 {$endpoint} \n";
} else {
echo "推送失败: {$report->getReason()} \n";
}
}
各平台踩坑实录
iOS Safari PWA
- 只有添加到主屏幕成为PWA应用,锁屏后台才能收到离线推送。仅仅打开Safari网页标签,锁屏后无法接收离线推送。

- 通知栏强制展示
from XXX,来源取自 manifest.json 的 name 字段,推送 payload 无法修改也不能隐藏该行文字。 - 不支持自定义通知铃声,sound 参数会被APNs直接丢弃,只能使用系统默认铃声。
- 如果手动上滑彻底关闭PWA进程,后台推送将无法送达。
- 修改manifest里应用名称后,要删除桌面图标,重新添加PWA并重新订阅通知权限。
国内国行安卓(小米 / OPPO / vivo / 华为)
WebPush 底层依赖谷歌FCM服务,国内网络无法访问FCM。
- 网页或者PWA处于前台运行状态:推送可以正常弹窗。
- 锁屏、切后台、浏览器被系统省电策略杀掉进程:无法收到离线推送,纯网页没有任何方案可以绕过该限制。
- 仅Chrome、Edge、Kiwi浏览器支持WebPush;UC、夸克、QQ浏览器均不兼容。
- 搭载GMS的海外安卓设备,整套代码直接运行,没有来源提示文字,系统层面可以自定义通知铃声。
业务选型参考
- 用户群体以iPhone为主:直接使用这套PWA+WebPush方案。国内安卓做降级处理,用户打开网页读取未读消息。
- 如果要求国内安卓锁屏离线接收推送:纯网页/PWA实现不了,需要打包APK,使用uni‑app这类框架接入Uni‑Push2.0等厂商推送SDK。
问题排查清单
遇到推送异常,逐项核对下面的点:
- 线上站点没有配置HTTPS;
- service‑worker.js 文件不在网站根目录;
- VAPID公私钥配置错误,密钥跨域名复用;
- iPhone没有把网页添加到主屏幕PWA;
- iPhone开启专注模式、勿扰模式或者静音;
- 国内国行安卓设备,无GMS谷歌服务,锁屏离线推送必然失效。
欢迎留言