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)
相关推荐
白水清风3 小时前
微前端学习记录(qiankun、wujie、micro-app)
前端·javascript·前端工程化
Ticnix3 小时前
函数封装实现Echarts多表渲染/叠加渲染
前端·echarts
用户22152044278003 小时前
new、原型和原型链浅析
前端·javascript
阿星做前端3 小时前
coze源码解读: space develop 页面
前端·javascript
叫我小窝吧3 小时前
Promise 的使用
前端·javascript
NBtab3 小时前
Vite + Vue3项目版本更新检查与页面自动刷新方案
前端
bobz9653 小时前
virtio vs vfio
后端
天天扭码3 小时前
来全面地review一下Flex布局(面试可用)
前端·css·面试
Rexi4 小时前
“Controller→Service→DAO”三层架构
后端
用户458203153174 小时前
CSS特异性:如何精准控制样式而不失控?
前端·css