📌 目标
为 Nuxt 3 项目添加以下功能:
- 用户启用/禁用 PWA 通知(前端开关)
- 浏览器请求推送权限
- 用户同意后,将订阅信息(PushSubscription JSON)保存到后端数据库
- 后端使用
web-push
发送通知
🧩 技术栈
- Nuxt 3
@vite-pwa/nuxt
插件- 浏览器 Push API
- Web Push 协议
- Node.js +
web-push
库 - Prisma + MySQL(或 PostgreSQL)
📁 项目结构预览
swift
/public/sw.js // 自定义 Service Worker
/server/api/push-subscribe // 接收客户端订阅 JSON
/server/utils/push.ts // 使用 web-push 发送通知
/composables/usePushSubscribe.ts
/prisma/schema.prisma
1️⃣ 安装 PWA 支持
bash
npm install @vite-pwa/nuxt
修改 nuxt.config.ts
ts
export default defineNuxtConfig({
modules: ['@vite-pwa/nuxt'],
runtimeConfig: {
public: {
vapidPublicKey: process.env.VAPID_PUBLIC_KEY
},
vapidPrivateKey: process.env.VAPID_PRIVATE_KEY,
vapidSubject: process.env.VAPID_SUBJECT
},
pwa: {
strategies: 'injectManifest',
registerType: 'autoUpdate',
srcDir: 'public',
filename: 'sw.js',
manifest: {
name: '过期提醒',
short_name: '提醒',
start_url: '/',
display: 'standalone',
theme_color: '#ffffff',
},
devOptions: {
enabled: process.env.NODE_ENV === 'development',
},
},
})
2️⃣ 设置环境变量 .env
env
VAPID_PUBLIC_KEY=你的VAPID公钥
VAPID_PRIVATE_KEY=你的VAPID私钥
VAPID_SUBJECT=mailto:admin@example.com
使用 web-push
生成 VAPID 密钥:
perl
bash
复制编辑
npx web-push generate-vapid-keys
3️⃣ 前端逻辑:订阅并发送给后端
composables/usePushSubscribe.ts
ts
function urlBase64ToUint8Array(base64String: string): Uint8Array { ... }
export async function usePushSubscribe(data: { id: number, value: boolean }) {
const config = useRuntimeConfig()
const publicKey = config.public.vapidPublicKey
// 请求权限
const permission = await Notification.requestPermission()
if (permission !== 'granted') {
throw new Error('通知权限被拒绝')
}
const registration = await navigator.serviceWorker.ready
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(publicKey),
})
// 发送给后端保存
await $fetch('/api/push-subscribe', {
method: 'POST',
body: {
id: data.id,
pwaEnabled: data.value,
subscription,
}
})
}
4️⃣ 后端接口:保存订阅信息
/server/api/push-subscribe.ts
ts
export default defineEventHandler(async (event) => {
const body = await readBody(event)
const { id, pwaEnabled, subscription } = body
// 保存到数据库
await prisma.notification.upsert({
where: { id },
update: {
pwaEnabled,
subscription: subscription,
},
create: {
id,
pwaEnabled,
subscription: subscription,
},
})
return { success: true }
})
5️⃣ 数据模型
prisma/schema.prisma
prisma
model Notification {
id Int @id @default(autoincrement())
pwaEnabled Boolean @default(false)
subscription Json?
updatedAt DateTime @updatedAt
}
6️⃣ 后端发送通知
/server/utils/push.ts
ts
import webPush from 'web-push'
import { useRuntimeConfig } from '#imports'
export async function sendNotification(subscription: any, payload: object) {
const config = useRuntimeConfig()
webPush.setVapidDetails(
config.vapidSubject,
config.vapidPublicKey,
config.vapidPrivateKey
)
await webPush.sendNotification(subscription, JSON.stringify(payload))
}
调用示例(在 API 或定时任务中):
ts
await sendNotification(user.subscription, {
title: '任务提醒',
body: '你有一条未完成的任务',
})
7️⃣ 自定义 public/sw.js
js
self.addEventListener('push', (event) => {
const data = event.data.json()
event.waitUntil(
self.registration.showNotification(data.title, {
body: data.body,
icon: '/icon.png',
})
)
})
✅ 结果
- 用户点击 UI 切换开关触发通知订阅
- 浏览器会请求权限
- 后端记录订阅信息
- 后台可通过
web-push
发送通知到用户浏览器
🔒 安全提示
- 不要将
VAPID_PRIVATE_KEY
注入到前端,只用于服务端 - 每个订阅对象是唯一的,按用户 ID 存储或哈希索引
- 注意兼容性问题(Safari 不支持 Web Push)