Nuxt 3 + PWA 通知完整实现指南(Web Push)

📌 目标

为 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)
相关推荐
今禾几秒前
Zustand状态管理(下):从基础到高级应用
前端·react.js·前端框架
涡能增压发动积3 分钟前
Browser-Use Agent使用初体验
人工智能·后端·python
gnip7 分钟前
js模拟重载
前端·javascript
Naturean10 分钟前
Web前端开发基础知识之查漏补缺
前端
curdcv_po11 分钟前
🔥 3D开发,自定义几何体 和 添加纹理
前端
单身汪v16 分钟前
告别混乱:前端时间与时区实用指南
前端·javascript
探索java24 分钟前
Spring lookup-method实现原理深度解析
java·后端·spring
lxsy31 分钟前
spring-ai-alibaba 之 graph 槽点
java·后端·spring·吐槽·ai-alibaba
鹏程十八少42 分钟前
2. Android 深度剖析LeakCanary:从原理到实践的全方位指南
前端
码事漫谈42 分钟前
深入解析线程同步中WaitForSingleObject的超时问题
后端