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)
相关推荐
谢慧琼16 小时前
2026零基础做企业网站,低成本搭建官方网站
服务器·前端·javascript
weixin_4316004417 小时前
前端数据埋点(1):一次点击如何变成一条埋点
前端·js·数据埋点
IT_陈寒18 小时前
Python线程池吞了异常还不告诉我,这谁顶得住啊
前端·人工智能·后端
小强库计算机毕业设计18 小时前
SpringBoot+Vue3 学生宿舍管理系统实战
java·spring boot·后端·vue·学生宿舍管理系统
小狼18318 小时前
实践6|SDD 实战:AI 写的规格被推翻了四次
后端·claude
篮框坏了18 小时前
"DeepSeek Harness 实测:一毛钱干三活,但默认配置是个坑"
后端·ai编程
计算机魔术师18 小时前
同样是AI辅助建造,为什么有的游戏三个月就烂尾了?
前端
jobBridge2118 小时前
大模型到底是怎么"想"的?我把 Transformer 拆开,发现它其实是个"接词狂魔"
人工智能·后端·编程语言
用户9210802628618 小时前
0. 做 Agent 产品,前端 AI 组件框架怎么选?
前端
唐青枫19 小时前
看懂内存地址之后,才算真正入门 Zig:指针、切片与实战
后端