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)
相关推荐
凌泽12 分钟前
写了那么多年的代码,我开始写“规范”了:AI 驱动的开发范式革命
前端·vibecoding
没有鸡汤吃不下饭13 分钟前
解决前端项目中大数据复杂列表场景的完美方案
前端·javascript·vue.js
Java之路行者19 分钟前
Spring Boot防重复提交实战:让接口安全提升200%!
spring boot·后端·安全
旧雨散尘37 分钟前
【react】react初学6-第一个react应用-待办事项
前端·react.js·前端框架
岁月向前1 小时前
iOS基础问题整理
前端
陈随易1 小时前
改变世界的编程语言MoonBit:配置系统介绍(下)
前端·后端·程序员
知其然亦知其所以然1 小时前
SpringAI + ONNX:打造不花钱、不联网的向量引擎!
后端·spring·aigc
jump6801 小时前
【react】 useReducer 集中式管理组件的状态
前端
许泽宇的技术分享1 小时前
把 CLI 搬上 Web:在内网打造“可二开”的 AI IDE,为什么这条路更现实?
前端·ide·人工智能
priority_key1 小时前
TCP 如何保证传输的可靠性?
服务器·网络·后端·网络协议·tcp/ip