برنامه نویسی

Web Push API

Web Push API

19 دقیقه زمان برای خواندن این مطلب نیاز است.

فهرست مطالب


Web Push API: راهنمای جامع و تخصصی ارسال نوتیفیکیشن‌های بومی در وب

در دنیای مدرن وب، تعامل مجدد با کاربران یکی از بزرگترین چالش‌های توسعه‌دهندگان است. برخلاف اپلیکیشن‌های موبایل، وب‌سایت‌ها به راحتی نمی‌توانند کاربران را از رویدادهای جدید مطلع کنند. اینجاست که Web Push API وارد می‌شود. Web Push API یک استاندارد وب قدرتمند است که به وب‌سایت‌ها اجازه می‌دهد نوتیفیکیشن‌های بومی (Native Notifications) را حتی زمانی که مرورگر بسته است، به کاربران ارسال کنند. اما Web Push API دقیقاً چیست و چرا باید به آن توجه کنیم؟ در پاسخ باید گفت: Web Push API ترکیبی از دو فناوری اصلی است: Push API (دریافت پیام از سرور) و Notifications API (نمایش نوتیفیکیشن به کاربر). با استفاده از Web Push API، شما می‌توانید پیام‌هایی را از سرور خود به مرورگر کاربر، حتی زمانی که وب‌سایت باز نیست، ارسال کنید و سپس آن پیام را به صورت یک نوتیفیکیشن بومی نمایش دهید. در این مقاله از دانا پدیا، قصد داریم به صورت عمیق و تخصصی به Web Push API بپردازیم. از مفاهیم پایه و معماری آن گرفته تا پیاده‌سازی کامل در سمت کلاینت (JavaScript) و سرور (Node.js، Python، PHP)، مدیریت اشتراک‌ها، رمزگذاری پیام‌ها، و بهترین شیوه‌ها را پوشش خواهیم داد.

اگر شما یک توسعه‌دهنده وب هستید که به دنبال افزایش تعامل کاربران، کاهش نرخ بازگشت (Bounce Rate)، و بازگرداندن کاربران به وب‌سایت خود هستید، این مقاله از دانا پدیا دقیقاً برای شما نوشته شده است. Web Push API توسط مرورگرهای مدرن مانند Chrome، Firefox، Safari، Edge و Opera پشتیبانی می‌شود و شرکت‌های بزرگی مانند Facebook، Twitter، و GitHub از آن استفاده می‌کنند. پس با ما همراه باشید تا سفری حرفه‌ای در دنیای Web Push API را آغاز کنیم.

IndexedDB در React


Web Push API چیست و چگونه کار می‌کند؟

قبل از پرداختن به جزئیات Web Push API، باید درک کنیم که این فناوری دقیقاً چیست و چه معماری دارد.

تعریف Web Push API

Web Push API مجموعه‌ای از استانداردهای وب (توسط W3C) است که به اپلیکیشن‌های وب اجازه می‌دهد پیام‌های فوری (Real-time) را از سرور به مرورگر کاربر، حتی زمانی که وب‌سایت باز نیست، ارسال کنند. این پیام‌ها سپس توسط مرورگر به عنوان نوتیفیکیشن بومی نمایش داده می‌شوند.

معماری Web Push API

معماری Web Push API از چهار جزء اصلی تشکیل شده است:

  1. کلاینت (Client) – Service Worker: یک فایل جاوااسکریپت که در پس‌زمینه مرورگر اجرا می‌شود و مسئول دریافت و نمایش نوتیفیکیشن‌ها است.
  2. سرور اپلیکیشن (Application Server): سرور شما که درخواست ارسال نوتیفیکیشن را آغاز می‌کند.
  3. Push Service: یک سرویس خارجی (مانند Firebase Cloud Messaging (FCM) برای Chrome، Apple Push Notification Service (APNS) برای Safari، و Mozilla Push Service برای Firefox) که پیام‌ها را بین سرور اپلیکیشن و مرورگر کاربر منتقل می‌کند.
  4. مرورگر (Browser): کلاینت نهایی که نوتیفیکیشن را دریافت و نمایش می‌دهد.

جریان کاری Web Push API

1. کاربر از وب‌سایت دیدن می‌کند.
2. وب‌سایت درخواست مجوز ارسال نوتیفیکیشن می‌کند.
3. کاربر مجوز می‌دهد (Allow).
4. Service Worker ثبت می‌شود و یک اشتراک (PushSubscription) از مرورگر دریافت می‌کند.
5. اشتراک (شامل endpoint و کلیدهای رمزنگاری) به سرور شما ارسال می‌شود.
6. سرور این اشتراک را ذخیره می‌کند.
7. زمانی که می‌خواهید نوتیفیکیشن بفرستید، سرور یک پیام رمزگذاری شده به Push Service (با استفاده از endpoint) ارسال می‌کند.
8. Push Service پیام را به مرورگر کاربر ارسال می‌کند.
9. Service Worker پیام را دریافت کرده و نوتیفیکیشن را نمایش می‌دهد.
10. کاربر روی نوتیفیکیشن کلیک می‌کند و به URL مشخص شده هدایت می‌شود.

مزایای Web Push API

  1. دسترسی بدون نیاز به اپلیکیشن موبایل: برخلاف نوتیفیکیشن‌های بومی موبایل، Web Push API روی دسکتاپ و موبایل از طریق مرورگر کار می‌کند.
  2. ارسال پیام حتی در صورت بسته بودن مرورگر: برخلاف WebSockets یا Server-Sent Events (SSE) که نیاز به صفحه باز دارند، Web Push API می‌تواند حتی زمانی که مرورگر بسته است (اما در حال اجرا در پس‌زمینه) پیام بفرستد.
  3. صرفه‌جویی در مصرف باتری و منابع: Push Service بهینه شده است و برخلاف WebSocket نیازی به نگهداری کانکشن دائمی ندارد.
  4. پشتیبانی از رمزگذاری سرتاسر (End-to-End Encryption): پیام‌ها قبل از ارسال به Push Service رمزگذاری می‌شوند و فقط مرورگر مقصد می‌تواند آنها را رمزگشایی کند.
  5. افزایش تعامل کاربران: نوتیفیکیشن‌های بومی نرخ بازگشت (Return Rate) را تا ۴ برابر افزایش می‌دهند.

الگوی طراحی Template Method

Web Push API

آشنایی با Service Workers (پیش‌نیاز Web Push API)

برای استفاده از Web Push API، درک Service Worker ضروری است. Service Worker یک کارگر جاوااسکریپت است که در پس‌زمینه مرورگر، مستقل از صفحه وب، اجرا می‌شود.

ویژگی‌های کلیدی Service Worker

  • بدون دسترسی به DOM: Service Worker به شی document یا DOM دسترسی ندارد.
  • رویدادمحور (Event-driven): در پاسخ به رویدادهایی مانند push (دریافت پیام push) یا notificationclick (کلیک روی نوتیفیکیشن) فعال می‌شود.
  • چرخه حیات جداگانه: Service Worker پس از نصب (install) و فعال شدن (activate)، تا زمانی که مرورگر آن را غیرفعال نکند، باقی می‌ماند.
  • فقط روی HTTPS (یا localhost): به دلایل امنیتی، Service Worker فقط در صفحات HTTPS یا localhost کار می‌کند.

چرخه حیات Service Worker

// 1. ثبت Service Worker
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js')
    .then(registration => {
      console.log('Service Worker ثبت شد:', registration);
    })
    .catch(error => {
      console.error('خطا در ثبت Service Worker:', error);
    });
}

// 2. رویدادهای داخل sw.js (فایل Service Worker)
self.addEventListener('install', (event) => {
  console.log('Service Worker نصب شد');
  // ذخیره منابع در کش (برای PWA)
});

self.addEventListener('activate', (event) => {
  console.log('Service Worker فعال شد');
  // پاکسازی کش‌های قدیمی
});

self.addEventListener('push', (event) => {
  console.log('پیام Push دریافت شد', event);
  // نمایش نوتیفیکیشن
});

self.addEventListener('notificationclick', (event) => {
  console.log('روی نوتیفیکیشن کلیک شد', event);
  // باز کردن لینک مورد نظر
});

API Throttling در Node.js


پیاده‌سازی کامل Web Push API در سمت کلاینت

حالا بیایید Web Push API را از صفر پیاده‌سازی کنیم. ابتدا سمت کلاینت (مرورگر) را می‌سازیم.

درخواست مجوز و ثبت Service Worker

// client.js
const PUBLIC_VAPID_KEY = 'YOUR_PUBLIC_VAPID_KEY'; // بعداً تولید می‌کنیم

async function registerServiceWorker() {
  if (!('serviceWorker' in navigator)) {
    console.error('مرورگر از Service Worker پشتیبانی نمی‌کند');
    return false;
  }

  if (!('PushManager' in window)) {
    console.error('مرورگر از Web Push API پشتیبانی نمی‌کند');
    return false;
  }

  try {
    // ثبت Service Worker
    const registration = await navigator.serviceWorker.register('/sw.js');
    console.log('Service Worker ثبت شد:', registration);
    return registration;
  } catch (error) {
    console.error('خطا در ثبت Service Worker:', error);
    return false;
  }
}

async function subscribeToPushNotifications() {
  // درخواست مجوز از کاربر
  const permission = await Notification.requestPermission();

  if (permission !== 'granted') {
    console.error('کاربر مجوز نوتیفیکیشن را نداد');
    alert('برای دریافت نوتیفیکیشن، لطفاً مجوز را بدهید.');
    return null;
  }

  const registration = await registerServiceWorker();
  if (!registration) return null;

  try {
    // دریافت اشتراک push
    const subscription = await registration.pushManager.subscribe({
      userVisibleOnly: true,  // الزامی (در Chrome)
      applicationServerKey: urlBase64ToUint8Array(PUBLIC_VAPID_KEY)
    });

    console.log('اشتراک push ایجاد شد:', subscription);

    // ارسال اشتراک به سرور
    await saveSubscriptionToServer(subscription);

    return subscription;
  } catch (error) {
    console.error('خطا در ایجاد اشتراک:', error);
    return null;
  }
}

// تبدیل VAPID key از base64 به Uint8Array (الزامی برای Chrome)
function urlBase64ToUint8Array(base64String) {
  const padding = '='.repeat((4 - base64String.length % 4) % 4);
  const base64 = (base64String + padding)
    .replace(/\-/g, '+')
    .replace(/_/g, '/');

  const rawData = window.atob(base64);
  const outputArray = new Uint8Array(rawData.length);

  for (let i = 0; i < rawData.length; ++i) {
    outputArray[i] = rawData.charCodeAt(i);
  }
  return outputArray;
}

async function saveSubscriptionToServer(subscription) {
  const response = await fetch('/api/save-subscription', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(subscription)
  });

  if (!response.ok) {
    throw new Error('خطا در ذخیره اشتراک در سرور');
  }

  console.log('اشتراک در سرور ذخیره شد');
}

// افزودن دکمه برای فعال‌سازی نوتیفیکیشن
document.getElementById('enable-notifications').addEventListener('click', () => {
  subscribeToPushNotifications();
});

فایل Service Worker (sw.js)

// sw.js
self.addEventListener('install', (event) => {
  console.log('Service Worker نصب شد');
  self.skipWaiting(); // بلافاصله فعال شود
});

self.addEventListener('activate', (event) => {
  console.log('Service Worker فعال شد');
  event.waitUntil(clients.claim()); // کنترل همه تب‌ها را بگیر
});

self.addEventListener('push', (event) => {
  console.log('رویداد push دریافت شد', event);

  let data = {
    title: 'پیام جدید',
    body: 'شما یک پیام جدید دارید',
    icon: '/icon.png',
    badge: '/badge.png',
    url: '/'
  };

  // اگر سرور داده ارسال کرده باشد
  if (event.data) {
    try {
      data = event.data.json();
    } catch (e) {
      data.body = event.data.text();
    }
  }

  // گزینه‌های نوتیفیکیشن
  const options = {
    body: data.body,
    icon: data.icon || '/icon.png',
    badge: data.badge || '/badge.png',
    vibrate: [200, 100, 200], // لرزش در دستگاه‌های پشتیبانی‌کننده
    data: {
      url: data.url || '/',
      timestamp: Date.now()
    },
    actions: [
      {
        action: 'open',
        title: 'باز کردن',
        icon: '/open-icon.png'
      },
      {
        action: 'dismiss',
        title: 'بستن',
        icon: '/close-icon.png'
      }
    ],
    tag: 'unique-tag', // برای جلوگیری از نوتیفیکیشن‌های تکراری
    renotify: false,    // اگر true، نوتیفیکیشن تکراری را دوباره نشان بده
    silent: false,      // اگر true، صدا نده
    requireInteraction: true, // تا کلیک کاربر بماند
    timestamp: Date.now()
  };

  event.waitUntil(
    self.registration.showNotification(data.title, options)
  );
});

self.addEventListener('notificationclick', (event) => {
  console.log('روی نوتیفیکیشن کلیک شد', event);

  const notification = event.notification;
  const action = event.action;
  const url = notification.data?.url || '/';

  notification.close(); // بستن نوتیفیکیشن

  if (action === 'dismiss') {
    // کاربر روی دکمه بستن کلیک کرده، کاری نکن
    return;
  }

  // باز کردن یا فوکوس کردن پنجره/تب موجود
  event.waitUntil(
    clients.matchAll({ type: 'window', includeUncontrolled: true })
      .then(windowClients => {
        // آیا پنجره‌ای با همین URL باز است؟
        const matchingClient = windowClients.find(client => client.url.includes(url));

        if (matchingClient) {
          // فوکوس روی پنجره موجود
          return matchingClient.focus();
        } else {
          // باز کردن پنجره جدید
          return clients.openWindow(url);
        }
      })
  );
});

// (اختیاری) مدیریت بسته شدن نوتیفیکیشن بدون کلیک
self.addEventListener('notificationclose', (event) => {
  console.log('نوتیفیکیشن بسته شد بدون کلیک', event);
  // می‌توانید آنالیتیکس ارسال کنید
});

دکمه فعال‌سازی در HTML

<!DOCTYPE html>
<html>
<head>
    <title>Web Push API - دانا پدیا</title>
    <link rel="manifest" href="/manifest.json">
</head>
<body>
    <h1>Web Push API</h1>
    <button id="enable-notifications">فعال‌سازی نوتیفیکیشن‌ها</button>
    <script src="/client.js"></script>
</body>
</html>

فایل manifest.json (برای PWA و آیکون)

{
  "name": "وب‌سایت من",
  "short_name": "وب‌سایت",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#000000",
  "icons": [
    {
      "src": "/icon-192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "/icon-512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ]
}

AsyncLocalStorage در Node.js


پیاده‌سازی سمت سرور برای Web Push API (Node.js)

حالا که سمت کلاینت Web Push API را پیاده‌سازی کردیم، باید سمت سرور را بسازیم تا بتوانیم نوتیفیکیشن بفرستیم.

نصب وابستگی‌ها

npm install web-push express body-parser

تولید کلیدهای VAPID

VAPID (Voluntary Application Server Identification) یک استاندارد برای شناسایی سرور اپلیکیشن و افزایش امنیت است.

// generate-vapid-keys.js
const webpush = require('web-push');

const vapidKeys = webpush.generateVAPIDKeys();

console.log('Public Key:', vapidKeys.publicKey);
console.log('Private Key:', vapidKeys.privateKey);

// این کلیدها را در متغیرهای محیطی ذخیره کنید

سرور Express برای Web Push API

// server.js
const express = require('express');
const bodyParser = require('body-parser');
const webpush = require('web-push');

const app = express();
app.use(bodyParser.json());

// تنظیم کلیدهای VAPID (از متغیرهای محیطی)
const vapidKeys = {
  publicKey: process.env.VAPID_PUBLIC_KEY,
  privateKey: process.env.VAPID_PRIVATE_KEY
};

webpush.setVapidDetails(
  'mailto:your-email@example.com', // ایمیل شما برای تماس در صورت مشکل
  vapidKeys.publicKey,
  vapidKeys.privateKey
);

// ذخیره اشتراک‌ها در حافظه (در واقعیت از دیتابیس استفاده کنید)
let subscriptions = [];

// endpoint برای ذخیره اشتراک
app.post('/api/save-subscription', (req, res) => {
  const subscription = req.body;

  // بررسی وجود اشتراک تکراری
  const exists = subscriptions.some(sub => sub.endpoint === subscription.endpoint);
  if (!exists) {
    subscriptions.push(subscription);
    console.log(`اشتراک جدید: ${subscription.endpoint}`);
  }

  res.json({ success: true, message: 'اشتراک ذخیره شد' });
});

// endpoint برای ارسال نوتیفیکیشن به همه کاربران
app.post('/api/send-notification', async (req, res) => {
  const { title, body, icon, url } = req.body;

  const payload = JSON.stringify({
    title: title || 'پیام جدید',
    body: body || 'شما یک پیام جدید دارید',
    icon: icon || '/icon.png',
    badge: '/badge.png',
    url: url || '/'
  });

  const results = [];

  for (const subscription of subscriptions) {
    try {
      await webpush.sendNotification(subscription, payload);
      results.push({ endpoint: subscription.endpoint, status: 'success' });
    } catch (error) {
      console.error(`خطا در ارسال به ${subscription.endpoint}:`, error);

      // اگر اشتراک منقضی شده باشد (410 Gone)
      if (error.statusCode === 410) {
        // حذف اشتراک منقضی
        subscriptions = subscriptions.filter(sub => sub.endpoint !== subscription.endpoint);
        results.push({ endpoint: subscription.endpoint, status: 'expired' });
      } else {
        results.push({ endpoint: subscription.endpoint, status: 'error', error: error.message });
      }
    }
  }

  res.json({
    success: true,
    results,
    total: subscriptions.length
  });
});

// endpoint برای ارسال به یک کاربر خاص (با endpoint)
app.post('/api/send-to-user', async (req, res) => {
  const { endpoint, title, body, icon, url } = req.body;

  const subscription = subscriptions.find(sub => sub.endpoint === endpoint);
  if (!subscription) {
    return res.status(404).json({ error: 'اشتراک یافت نشد' });
  }

  const payload = JSON.stringify({
    title: title || 'پیام جدید',
    body: body || 'شما یک پیام جدید دارید',
    icon: icon || '/icon.png',
    url: url || '/'
  });

  try {
    await webpush.sendNotification(subscription, payload);
    res.json({ success: true, message: 'نوتیفیکیشن ارسال شد' });
  } catch (error) {
    if (error.statusCode === 410) {
      // حذف اشتراک منقضی
      subscriptions = subscriptions.filter(sub => sub.endpoint !== endpoint);
    }
    res.status(500).json({ error: error.message });
  }
});

// صفحه اصلی
app.get('/', (req, res) => {
  res.sendFile(__dirname + '/index.html');
});

// فایل‌های استاتیک
app.use(express.static('public'));

app.listen(3000, () => {
  console.log('سرور Web Push API روی پورت 3000 اجرا شد');
  console.log('Public Key:', vapidKeys.publicKey);
});

تست Accessibility با Axe


پیاده‌سازی سمت سرور با زبان‌های دیگر

Python (با pywebpush)

# server.py
from flask import Flask, request, jsonify
from pywebpush import webpush, WebPushException
import json

app = Flask(__name__)

VAPID_PRIVATE_KEY = "YOUR_PRIVATE_KEY"
VAPID_PUBLIC_KEY = "YOUR_PUBLIC_KEY"
VAPID_CLAIMS = {
    "sub": "mailto:your-email@example.com"
}

subscriptions = []

@app.route('/api/save-subscription', methods=['POST'])
def save_subscription():
    subscription = request.json
    if subscription not in subscriptions:
        subscriptions.append(subscription)
    return jsonify({"success": True})

@app.route('/api/send-notification', methods=['POST'])
def send_notification():
    data = request.json
    title = data.get('title', 'پیام جدید')
    body = data.get('body', 'شما یک پیام جدید دارید')
    icon = data.get('icon', '/icon.png')
    url = data.get('url', '/')

    payload = json.dumps({
        'title': title,
        'body': body,
        'icon': icon,
        'url': url
    })

    results = []
    for sub in subscriptions:
        try:
            webpush(
                subscription_info=sub,
                data=payload,
                vapid_private_key=VAPID_PRIVATE_KEY,
                vapid_claims=VAPID_CLAIMS
            )
            results.append({"endpoint": sub['endpoint'], "status": "success"})
        except WebPushException as e:
            if e.response and e.response.status_code == 410:
                subscriptions.remove(sub)
                results.append({"endpoint": sub['endpoint'], "status": "expired"})
            else:
                results.append({"endpoint": sub['endpoint'], "status": "error"})

    return jsonify({"success": True, "results": results})

if __name__ == '__main__':
    app.run(port=3000)

PHP (با web-push-php)

<?php
// server.php
require_once 'vendor/autoload.php';

use Minishlink\WebPush\WebPush;
use Minishlink\WebPush\Subscription;

$auth = [
    'VAPID' => [
        'subject' => 'mailto:your-email@example.com',
        'publicKey' => 'YOUR_PUBLIC_KEY',
        'privateKey' => 'YOUR_PRIVATE_KEY'
    ]
];

$webPush = new WebPush($auth);

// ذخیره اشتراک (در واقعیت از دیتابیس استفاده کنید)
$subscriptions = [];

if ($_SERVER['REQUEST_METHOD'] === 'POST' && $_SERVER['REQUEST_URI'] === '/api/save-subscription') {
    $subscription = json_decode(file_get_contents('php://input'), true);
    $subscriptions[] = $subscription;
    echo json_encode(['success' => true]);
    exit;
}

if ($_SERVER['REQUEST_METHOD'] === 'POST' && $_SERVER['REQUEST_URI'] === '/api/send-notification') {
    $data = json_decode(file_get_contents('php://input'), true);
    $payload = json_encode([
        'title' => $data['title'] ?? 'پیام جدید',
        'body' => $data['body'] ?? 'شما یک پیام جدید دارید',
        'icon' => $data['icon'] ?? '/icon.png',
        'url' => $data['url'] ?? '/'
    ]);

    foreach ($subscriptions as $sub) {
        $subscription = Subscription::create($sub);
        $webPush->queueNotification($subscription, $payload);
    }

    foreach ($webPush->flush() as $report) {
        if (!$report->isSuccess()) {
            if ($report->getEndpoint() === 'expired') {
                // حذف اشتراک منقضی
            }
        }
    }

    echo json_encode(['success' => true]);
    exit;
}
?>

Rust Web Framework Actix


مدیریت اشتراک‌ها و دیتابیس در Web Push API

در پروژه‌های واقعی، اشتراک‌ها باید در دیتابیس ذخیره شوند. در این بخش از دانا پدیا، یک مدل ساده Django برای ذخیره اشتراک‌ها ارائه می‌دهیم.

مدل Django برای Push Subscription

# models.py
from django.db import models
from django.contrib.auth.models import User

class PushSubscription(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE, null=True, blank=True)
    endpoint = models.URLField(max_length=500, unique=True)
    auth_key = models.CharField(max_length=100)
    p256dh_key = models.CharField(max_length=200)
    user_agent = models.CharField(max_length=200, blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    is_active = models.BooleanField(default=True)

    class Meta:
        indexes = [
            models.Index(fields=['endpoint']),
            models.Index(fields=['user', 'is_active']),
        ]

    def __str__(self):
        return f"{self.user} - {self.endpoint[:50]}"

    def to_subscription_dict(self):
        """تبدیل به فرمت مورد نیاز web-push"""
        return {
            'endpoint': self.endpoint,
            'keys': {
                'auth': self.auth_key,
                'p256dh': self.p256dh_key
            }
        }

ارسال نوتیفیکیشن به کاربران خاص

# tasks.py (با استفاده از Celery)
from celery import shared_task
from webpush import webpush
from .models import PushSubscription

@shared_task
def send_notification_to_users(user_ids, title, body, url='/'):
    subscriptions = PushSubscription.objects.filter(
        user_id__in=user_ids,
        is_active=True
    )

    payload = {
        'title': title,
        'body': body,
        'icon': '/icon.png',
        'url': url
    }

    for sub in subscriptions:
        try:
            webpush(
                subscription_info=sub.to_subscription_dict(),
                data=json.dumps(payload),
                vapid_private_key=settings.VAPID_PRIVATE_KEY,
                vapid_claims={'sub': 'mailto:your-email@example.com'}
            )
        except Exception as e:
            # اگر اشتراک منقضی شده باشد
            if 'expired' in str(e).lower():
                sub.is_active = False
                sub.save()

Redis Caching Patterns


امنیت و رمزگذاری در Web Push API

یکی از مهمترین ویژگی‌های Web Push API، رمزگذاری سرتاسر (End-to-End Encryption) است. پیام‌ها قبل از ارسال به Push Service رمزگذاری می‌شوند و فقط مرورگر مقصد می‌تواند آنها را رمزگشایی کند.

چگونه رمزگذاری کار می‌کند؟

  1. کلاینت یک جفت کلید (public/private) در مرورگر تولید می‌کند.
  2. کلید عمومی (p256dh) به سرور ارسال می‌شود.
  3. سرور پیام را با کلید عمومی رمزگذاری می‌کند.
  4. پیام رمزگذاری شده به Push Service ارسال می‌شود.
  5. Push Service پیام را به مرورگر می‌فرستد.
  6. مرورگر با کلید خصوصی خود پیام را رمزگشایی می‌کند.

کتابخانه web-push به طور خودکار رمزگذاری را انجام می‌دهد. شما نیازی به مدیریت دستی ندارید.

نکات امنیتی اضافی

  • همیشه از HTTPS استفاده کنید: Push API فقط روی HTTPS کار می‌کند.
  • هرگز اطلاعات حساس را در نوتیفیکیشن نمایش ندهید: نوتیفیکیشن‌ها روی صفحه قفل (Lock Screen) نیز قابل مشاهده هستند.
  • از VAPID استفاده کنید: VAPID به Push Service اجازه می‌دهد سرور شما را شناسایی کند و از سوءاستفاده جلوگیری کند.
  • اشتراک‌های منقضی را پاک کنید: اگر کاربر از نوتیفیکیشن unsubscribe کند یا مرورگر را تغییر دهد، endpoint منقضی می‌شود.

کار با Prisma در Next.js

Web Push API

بهترین شیوه‌های Web Push API

بر اساس تجربیات تیم دانا پدیا، رعایت این نکات شما را در استفاده از Web Push API به یک حرفه‌ای تبدیل می‌کند:

۱. زمان مناسب برای درخواست مجوز

هرگز بلافاصله پس از بارگذاری صفحه، درخواست مجوز نکنید. منتظر یک اقدام معنادار از کاربر باشید (مانند کلیک روی دکمه، اسکرول به انتهای مقاله، یا ثبت‌نام).

// ❌ بد - بلافاصله
window.addEventListener('load', () => {
  Notification.requestPermission();
});

// ✅ خوب - پس از اقدام کاربر
document.getElementById('subscribe-btn').addEventListener('click', () => {
  subscribeToPushNotifications();
});

۲. شخصی‌سازی نوتیفیکیشن‌ها

از نام کاربر، محتوای مرتبط، و اطلاعات شخصی استفاده کنید تا نرخ کلیک افزایش یابد.

const payload = {
  title: `سلام ${userName}!`,
  body: `مطلب جدیدی که دنبال می‌کردید منتشر شد: "${postTitle}"`,
  icon: userAvatar,
  url: `/posts/${postId}`
};

۳. احترام به تنظیمات کاربر

اگر کاربر نوتیفیکیشن را رد کرد، دیگر درخواست ندهید. اگر چند بار رد کرد، حداقل ۳۰ روز صبر کنید.

if (Notification.permission === 'denied') {
  // دیگر درخواست نده
  return;
}

۴. استفاده از Tag برای جلوگیری از اسپم

از tag برای جایگزینی نوتیفیکیشن‌های قبلی استفاده کنید:

const options = {
  tag: 'new-message', // فقط آخرین پیام نمایش داده می‌شود
  renotify: false
};

۵. ارائه دکمه Unsubscribe

به کاربران اجازه دهید به راحتی نوتیفیکیشن‌ها را غیرفعال کنند.

async function unsubscribe() {
  const registration = await navigator.serviceWorker.ready;
  const subscription = await registration.pushManager.getSubscription();

  if (subscription) {
    await subscription.unsubscribe();
    await fetch('/api/delete-subscription', {
      method: 'POST',
      body: JSON.stringify({ endpoint: subscription.endpoint }),
      headers: { 'Content-Type': 'application/json' }
    });
    alert('نوتیفیکیشن‌ها غیرفعال شدند');
  }
}

۶. تست در مرورگرهای مختلف

  • Chrome: از FCM (Firebase Cloud Messaging) استفاده می‌کند.
  • Firefox: از Mozilla Push Service استفاده می‌کند.
  • Safari: از APNS (Apple Push Notification Service) استفاده می‌کند (نیاز به Apple Developer Account برای ارسال).
  • Edge: مانند Chrome از FCM استفاده می‌کند.

۷. آنالیز و ردیابی

نرخ کلیک (CTR)، نرخ اشتراک (Subscription Rate)، و نرخ لغو اشتراک (Unsubscribe Rate) را ردیابی کنید.

// ثبت رویدادها برای آنالیز
fetch('/api/analytics/push-click', {
  method: 'POST',
  body: JSON.stringify({ notificationId, action }),
  headers: { 'Content-Type': 'application/json' }
});

Hotwire Turbo چیست


عیب‌یابی مشکلات رایج در Web Push API

مشکل ۱: نوتیفیکیشن نمایش داده نمی‌شود

علت: کاربر مجوز نداده، Service Worker ثبت نشده، یا خطا در کد.

راه‌حل:

  • بررسی کنید Notification.permission === 'granted'
  • در DevTools > Application > Service Workers وضعیت را بررسی کنید
  • خطاهای console را بررسی کنید

مشکل ۲: خطای “Invalid VAPID key” در سرور

علت: کلید VAPID عمومی یا خصوصی اشتباه است.

راه‌حل: مجدداً کلیدها را تولید کنید و مطمئن شوید در کلاینت (applicationServerKey) و سرور (vapidDetails) یکسان هستند.

مشکل ۳: پیام به بعضی کاربران می‌رسد و به بعضی نمی‌رسد

علت: برخی مرورگرها (مانند Safari) نیاز به تنظیمات خاص دارند یا کاربر در حالت Incognito است.

راه‌حل: برای Safari، باید از Apple Push Notification Service استفاده کنید که نیاز به Apple Developer Account و گواهی دارد.

مشکل ۴: خطای 410 Gone در سرور

علت: اشتراک منقضی شده است (کاربر unsubscribe کرده یا مرورگر را تغییر داده).

راه‌حل: اشتراک را از دیتابیس حذف کنید.

مشکل ۵: نوتیفیکیشن در Android کار نمی‌کند

علت: مرورگرهای Android (مانند Chrome) نیاز به Google Play Services دارند.

راه‌حل: کاربر باید Google Play Services را فعال داشته باشد.


ابزارها و منابع مفید برای Web Push API

  • web-push (Node.js): کتابخانه رسمی برای سرور Node.js
  • pywebpush (Python): کتابخانه برای سرور Python
  • web-push-php (PHP): کتابخانه برای سرور PHP
  • OneSignal: سرویس آماده ارسال نوتیفیکیشن (تجاری)
  • Pushpad: سرویس آماده با پلن رایگان
  • VAPID Key Generator: ابزار آنلاین تولید کلید

CSS :where() و :is() Selectors


نتیجه‌گیری نهایی از دیدگاه دانا پدیا

در این مقاله جامع از دانا پدیا، ما به طور کامل Web Push API را از جنبه‌های مختلف بررسی کردیم. Web Push API یک فناوری انقلابی است که به وب‌سایت‌ها قدرت ارسال نوتیفیکیشن‌های بومی را می‌دهد و می‌تواند:

  • تعامل کاربران را تا ۴ برابر افزایش دهد.
  • نرخ بازگشت (Return Rate) را بهبود بخشد.
  • فروش و تبدیل (Conversion) را افزایش دهد.
  • کاربران را از رویدادهای مهم مطلع کند.

با استفاده از Web Push API و رعایت بهترین شیوه‌ها (درخواست مجوز در زمان مناسب، شخصی‌سازی، احترام به کاربر، و آنالیز)، می‌توانید تجربه کاربری وب‌سایت خود را به سطح جدیدی ببرید.

امیدواریم این مقاله از دانا پدیا برای شما مفید بوده باشد. اگر تجربه یا سؤالی درباره Web Push API دارید، در بخش نظرات با ما در میان بگذارید.

Storybook برای React


سوالات متداول (FAQ)

۱. آیا Web Push API در همه مرورگرها کار می‌کند؟

بیشتر مرورگرهای مدرن از Web Push API پشتیبانی می‌کنند: Chrome (از نسخه ۴۲)، Firefox (از نسخه ۴۴)، Edge (از نسخه ۱۷)، Opera (از نسخه ۲۹)، و Safari (از نسخه ۱۶.۱). تنها مرورگری که پشتیبانی نمی‌کند، Internet Explorer است. در مرورگرهای موبایل (Android Chrome, Firefox) نیز کار می‌کند. Safari در iOS از نسخه ۱۶.۴ به بعد پشتیبانی محدودی دارد.

۲. آیا برای ارسال نوتیفیکیشن به سرور شخص ثالث (Push Service) نیاز دارم؟

بله، Web Push API برای ارسال پیام به مرورگر کاربر به یک Push Service نیاز دارد. اما شما نیازی به ثبت‌نام جداگانه ندارید. هر مرورگر از Push Service مخصوص خود استفاده می‌کند:

  • Chrome و Edge: Firebase Cloud Messaging (FCM)
  • Firefox: Mozilla Push Service
  • Safari: Apple Push Notification Service (APNS)

کتابخانه web-push به طور خودکار با این سرویس‌ها ارتباط برقرار می‌کند.

۳. هزینه استفاده از Web Push API چقدر است؟

خود Web Push API رایگان است. اما اگر از سرور شخص ثالث مانند OneSignal یا Pushpad استفاده کنید، ممکن است هزینه داشته باشد (معمولاً برای تعداد نوتیفیکیشن‌های بالا). استفاده مستقیم از web-push و FCM رایگان است.

۴. آیا می‌توانم بدون VAPID کار کنم؟

از سال ۲۰۲۰، Chrome و Firefox نیاز به VAPID دارند. بدون VAPID، مرورگر نوتیفیکیشن را قبول نمی‌کند. VAPID به Push Service اجازه می‌دهد سرور شما را شناسایی کند و از سوءاستفاده جلوگیری کند. ایجاد VAPID key رایگان و آسان است.

۵. چگونه بفهمم کاربر نوتیفیکیشن را دیده یا روی آن کلیک کرده است؟

می‌توانید در Service Worker، رویدادهای notificationclick و notificationclose را ردیابی کنید و آنالیتیکس را به سرور ارسال کنید. همچنین می‌توانید از event.notification.data برای ارسال اطلاعات اضافی استفاده کنید.

۶. آیا Web Push API روی PWA (Progressive Web App) کار می‌کند؟

بله، Web Push API یکی از الزامات اصلی PWA است. ترکیب PWA + Web Push API تجربه‌ای شبیه اپلیکیشن موبایل را در وب فراهم می‌کند.

۷. چه حجمی از داده می‌توانم در هر نوتیفیکیشن ارسال کنم؟

حداکثر حجم پیام (payload) محدود است. در Chrome حدود ۴۰۹۶ بایت (۴ کیلوبایت) و در Firefox حدود ۴۰۹۶ بایت است. در Safari از طریق APNS، حداکثر ۴ کیلوبایت. برای داده‌های بیشتر، فقط یک شناسه ارسال کنید و داده اصلی را از API دریافت کنید.

۸. آیا کاربر می‌تواند نوتیفیکیشن را بعداً لغو کند؟

بله، کاربر می‌تواند از طریق تنظیمات مرورگر (Settings > Privacy > Notifications) نوتیفیکیشن را برای هر وب‌سایت غیرفعال کند. همچنین شما باید دکمه Unsubscribe را در وب‌سایت خود ارائه دهید.

۹. تفاوت Web Push API با WebSocket چیست؟

Web Push API برای ارسال نوتیفیکیشن‌های یک‌طرفه (سرور به کلاینت) حتی زمانی که صفحه بسته است طراحی شده است. WebSocket برای ارتباط دوطرفه (full-duplex) زمانی که صفحه باز است طراحی شده است. WebSocket نمی‌تواند زمانی که مرورگر بسته است پیام بفرستد.

۱۰. چگونه Web Push API را در اپلیکیشن موبایل (React Native, Flutter) پیاده‌سازی کنم؟

Web Push API مخصوص وب است. برای اپلیکیشن‌های موبایل بومی، باید از سرویس‌های مخصوص هر پلتفرم استفاده کنید (FCM برای Android و APNS برای iOS). اما می‌توانید از راه‌حل‌های یکپارچه مانند OneSignal یا Firebase Cloud Messaging استفاده کنید که هم وب و هم موبایل را پشتیبانی می‌کنند.

Kubernetes Service Mesh


دیدگاهتان را بنویسید