עם ניסיון של למעלה מ-5 שנים בהטמעת התראות push, אנו מבטיחים משלוח מאובטח ואמין. האינטגרציה הטכנית של Web Push נראית פשוטה עד שנתקלים בניואנסים של VAPID, פקיעת מינויים או חסימות דפדפן. הטמענו מנגנון זה בלמעלה מ-30 פרויקטים: מחנויות מקוונות עם מיליוני משתמשים ועד סטארטאפים עם קומץ משתמשים. ובכל פעם מצאנו מלכודות לא ברורות—לדוגמה, Safari ב-iOS לפני גרסה 16.4 פשוט מתעלם מ-push, ו-Chrome יכול למחוק מינויים בכוח ללא הודעה. במאמר זה, ננתח הטמעה אמיתית: מיצירת מפתחות VAPID ועד שליחה בצד השרת ב-Laravel וניטור משלוחים.
לפי מפרט Push API (MDN): "עובד שירות (Service Worker) חייב לטפל באירוע push כדי להציג התראה."
בעיות שאנו פותרים
חוסר תאימות דפדפנים אינו האתגר היחיד. התראות push בדפדפן פועלות לפי חוקים נוקשים: לא ניתן לשלוח הודעה ללא הרשאה מפורשת, וקל לאבד אותה. הנה נקודות הכאב העיקריות:
- פקיעת מינוי: הדפדפן מסיר אוטומטית נקודות קצה לא פעילות. ללא ניקוי קבוע, תבזבז משאבים על מינויים מתים—עד 30% ממאגר הנתונים עלול להיות לא תקף.
- התאמה אישית: התראות כלליות ללא פילוח מניבות CTR נמוך מ-10%. פילוח לפי פעולות (עגלה נטושה, חזרה, הישג) מעלה את התגובה פי שלושה—מ-8% ל-24%.
- שגיאות VAPID: פורמט מפתח לא נכון או subject שפג תוקפו מובילים לדחייה שקטה על ידי שירות ה-Push. זה גורם לכישלון של עד 15% מהשליחות.
VAPID הוא חובה מוחלטת כי זו הדרך היחידה לאמת את השרת מול שירות ה-Push. בלעדיו, בקשות פשוט מתעלמות. אנו מייצרים מפתחות פעם אחת—המפתח הפרטי נשמר ב-.env, והמפתח הציבורי מועבר לדפדפן במהלך המינוי.
כיצד הארכיטקטורה עובדת
השרת שלך → שירות Push (Google FCM, Mozilla Autopush) → דפדפן → עובד שירות → התראה
עובד השירות הוא פרוקסי בין השרת למשתמש. הוא עובד גם כשהאתר סגור, מטפל באירועי push ומציג התראות. קריטי ליישם נכון את הרישום שלו ואת טיפול בלחיצות.
השוואת ערוצי תקשורת
| ערוץ | CTR | מהירות משלוח | התאמה אישית | עלות |
|---|---|---|---|---|
| אימייל | ~20% | דקות-שעות | גבוהה | נמוכה |
| SMS | ~30% | שניות | בינונית | גבוהה |
| Web Push | ~60% | שניות | גבוהה | נמוכה (~$0.001 להודעה) |
Web Push מנצח בשלושה פרמטרים: זול יותר מ-SMS (חיסכון של עד $0.02 להודעה), מהיר יותר מאימייל, וניתן להתאמה אישית ללא אובדן מהירות. עבור חנות איקומרס טיפוסית עם 50,000 מינויים, שליחת 10 קמפיינים בחודש מביאה לעלות push מתחת ל-$10, לעומת SMS שעלותו $500—חיסכון של 98%. עכשיו להטמעה.
כיצד פילוח מעלה את ה-CTR?
פילוח לפי אירועי מוצר נותן עלייה של פי 2-3 ב-CTR. דוגמה: חנות מקוונת שהטמיעה התראות push מותאמות אישית על סטטוס הזמנה הגדילה ביקורים חוזרים ב-30% ואת ערך ההזמנה הממוצע ב-15%. השתמשו בתגיות: לעגלה נטושה—cart-abandoned, לחדשות—news, למבצעים—promo. כך לא מעמיסים על המשתמש ומגדילים רלוונטיות. הניסיון שלנו מראה שפילוח מעלה את ה-CTR פי 2-3, מ-10% ממוצע ל-30%.
מה לעשות עם שגיאות משלוח?
שגיאות משלוח מטופלות על ידי בדיקת קודי התגובה של שירות ה-Push: 410 (מינוי הוסר), 404 (לא נמצא), 429 (הגבלת קצב). בקוד Laravel (ראה סעיף שרת), אנו מוחקים אוטומטית נקודות קצה מתות לאחר קבלת 410. זה מפחית שליחות כושלות ב-20%. אם התראות לא מגיעות, בדקו את מפתחות ה-VAPID ואת פורמט נקודת הקצה.
מדריך הטמעה שלב אחר שלב
- צרו מפתחות VAPID:
npx web-push generate-vapid-keys, שמרו את המפתח הפרטי ב-.env. - רשמו עובד שירות בצד הלקוח: בקובץ
sw.jsנפרד, טפלו באירועיpushו-notificationclick. - רשמו את הדפדפן: השתמשו ב-
PushManager.subscribe()עם המפתח הציבורי של VAPID. שלחו את אובייקט המינוי לשרת. - אחסון בצד השרת: שמרו את נקודת הקצה,
p256dh, וטוקןauthבמאגר נתונים (לדוגמה, PostgreSQL). - הגדירו את Push API: צרו את ה-payload (כותרת, גוף, אייקון, url) ושלחו באמצעות ספריית
minishlink/web-push. - עבדו תוצאות: הסירו נקודות קצה שהחזירו שגיאה 410 או 404.
מידע נוסף על VAPID
VAPID (Voluntary Application Server Identification) הוא תקן IETF (RFC 8292). המפתח הפרטי חותם על טוקן שהדפדפן מעביר לשירות ה-Push. אם השרת לא מספק טוקן תקף, שירות ה-Push דוחה את הבקשה. ניתן ליצור מפתחות באמצעות `web-push` או ידנית באמצעות Elliptic Curve P-256.קוד מינוי ב-TypeScript
// push-subscription.ts
const VAPID_PUBLIC_KEY = import.meta.env.VITE_VAPID_PUBLIC_KEY;
function urlBase64ToUint8Array(base64String: string): Uint8Array {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
const rawData = window.atob(base64);
return Uint8Array.from([...rawData].map(char => char.charCodeAt(0)));
}
export async function subscribeToPush(): Promise<boolean> {
if (!('PushManager' in window)) {
console.warn('Push notifications not supported');
return false;
}
const permission = await Notification.requestPermission();
if (permission !== 'granted') return false;
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY),
});
await fetch('/api/push/subscribe', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(subscription),
});
return true;
}
export async function unsubscribeFromPush(): Promise<void> {
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.getSubscription();
if (subscription) {
await subscription.unsubscribe();
await fetch('/api/push/unsubscribe', {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ endpoint: subscription.endpoint }),
});
}
}
קוד מינוי push לדפדפן זה משתמש במפתחות VAPID וניתן לשימוש חוזר לכל פרויקט.
עובד שירות: טיפול ב-Push ולחיצות
// sw.js
self.addEventListener('push', event => {
const data = event.data?.json() ?? {};
const options = {
body: data.body ?? 'Новое уведомление',
icon: data.icon ?? '/icons/icon-192.png',
badge: '/icons/badge-72.png',
image: data.image,
tag: data.tag ?? 'default',
renotify: data.renotify ?? false,
data: { url: data.url ?? '/' },
actions: data.actions ?? [],
requireInteraction: data.requireInteraction ?? false,
};
event.waitUntil(
self.registration.showNotification(data.title ?? 'Уведомление', options)
);
});
self.addEventListener('notificationclick', event => {
event.notification.close();
const url = event.notification.data?.url ?? '/';
event.waitUntil(
clients.matchAll({ type: 'window', includeUncontrolled: true })
.then(windowClients => {
const existing = windowClients.find(c => c.url === url && 'focus' in c);
if (existing) return existing.focus();
return clients.openWindow(url);
})
);
});
טיפול זה בעובד שירות מבטיח שהתראות יופיעו גם כשהאתר סגור.
שרת ב-Laravel: מינוי, שליחה וניקוי
// Миграция таблицы push_subscriptions
Schema::create('push_subscriptions', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
$table->string('endpoint')->unique();
$table->string('public_key');
$table->string('auth_token');
$table->json('user_agent_data')->nullable();
$table->timestamps();
});
// Контроллер подписки
class PushSubscriptionController extends Controller {
public function subscribe(Request $request): JsonResponse {
$data = $request->validate([
'endpoint' => 'required|url',
'keys.p256dh' => 'required|string',
'keys.auth' => 'required|string',
]);
PushSubscription::updateOrCreate(
['endpoint' => $data['endpoint']],
[
'user_id' => auth()->id(),
'public_key' => $data['keys']['p256dh'],
'auth_token' => $data['keys']['auth'],
]
);
return response()->json(['status' => 'ok']);
}
}
// Класс отправки уведомлений
use Minishlink\WebPush\WebPush;
use Minishlink\WebPush\Subscription;
class SendPushNotification {
public function send(PushSubscription $sub, array $payload): void {
$webPush = new WebPush([
'VAPID' => [
'subject' => config('services.vapid.subject'),
'publicKey' => config('services.vapid.public_key'),
'privateKey' => config('services.vapid.private_key'),
],
]);
$webPush->queueNotification(
Subscription::create([
'endpoint' => $sub->endpoint,
'contentEncoding' => 'aesgcm',
'keys' => [
'p256dh' => $sub->public_key,
'auth' => $sub->auth_token,
],
]),
json_encode($payload)
);
foreach ($webPush->flush() as $report) {
if (!$report->isSuccess()) {
if ($report->isSubscriptionExpired()) {
PushSubscription::where('endpoint', $report->getEndpoint())->delete();
}
}
}
}
}מערכת התראות ה-push שלנו ב-Laravel משתלבת בצורה חלקה עם ספריית Minishlink WebPush להגדרת Push API.
תרחיש: התראות סטטוס הזמנה
המשתמש מבצע הזמנה—המערכת שולחת אוטומטית push: "הזמנה #123: הסטטוס שונה ל'נשלח'". זה מיושם באמצעות אירוע // push-subscription.ts const VAPID_PUBLIC_KEY = import.meta.env.VITE_VAPID_PUBLIC_KEY; function urlBase64ToUint8Array(base64String: string): Uint8Array { const padding = '='.repeat((4 - base64String.length % 4) % 4); const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/'); const rawData = window.atob(base64); return Uint8Array.from([...rawData].map(char => char.charCodeAt(0))); } export async function subscribeToPush(): Promise<boolean> { if (!('PushManager' in window)) { console.warn('Push notifications not supported'); return false; } const permission = await Notification.requestPermission(); if (permission !== 'granted') return false; const registration = await navigator.serviceWorker.ready; const subscription = await registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY), }); await fetch('/api/push/subscribe', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(subscription), }); return true; } export async function unsubscribeFromPush(): Promise<void> { const registration = await navigator.serviceWorker.ready; const subscription = await registration.pushManager.getSubscription(); if (subscription) { await subscription.unsubscribe(); await fetch('/api/push/unsubscribe', { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ endpoint: subscription.endpoint }), }); } } . לשם כך, אנו יוצרים seeder או listener שכאשר סטטוס ההזמנה משתנה, אוסף את המינויים של המשתמש ושולח דרך // sw.js self.addEventListener('push', event => { const data = event.data?.json() ?? {}; const options = { body: data.body ?? 'Новое уведомление', icon: data.icon ?? '/icons/icon-192.png', badge: '/icons/badge-72.png', image: data.image, tag: data.tag ?? 'default', renotify: data.renotify ?? false, data: { url: data.url ?? '/' }, actions: data.actions ?? [], requireInteraction: data.requireInteraction ?? false, }; event.waitUntil( self.registration.showNotification(data.title ?? 'Уведомление', options) ); }); self.addEventListener('notificationclick', event => { event.notification.close(); const url = event.notification.data?.url ?? '/'; event.waitUntil( clients.matchAll({ type: 'window', includeUncontrolled: true }) .then(windowClients => { const existing = windowClients.find(c => c.url === url && 'focus' in c); if (existing) return existing.focus(); return clients.openWindow(url); }) ); }); .
class OrderStatusChanged {
public function handle(Order $order): void {
$user = $order->user;
$subscriptions = PushSubscription::where('user_id', $user->id)->get();
foreach ($subscriptions as $sub) {
$this->sender->send($sub, [
'title' => 'Статус заказа изменён',
'body' => "Заказ #{$order->number}: {$order->status_label}",
'icon' => '/icons/order-icon.png',
'url' => "/account/orders/{$order->id}",
'tag' => "order-{$order->id}",
'renotify' => true,
'actions' => [
['action' => 'view', 'title' => 'Посмотреть заказ'],
['action' => 'dismiss', 'title' => 'Закрыть'],
],
]);
}
}
} מה אתם מקבלים
- קוד עובד שירות מלא וקוד מינוי בצד הלקוח ב-TypeScript.
- קוד בצד השרת ב-Laravel עם migrations, controller ומחלקת שליחה.
- אנליטיקת משלוח מוגדרת וניקוי אוטומטי של מינויים מתים.
- תיעוד: תיאור API, הוראות למנהל, גישה ללוגים.
- תמיכה למשך שבועיים לאחר ההטמעה.
שלבים ולוחות זמנים
| שלב | משך | תוצאה |
|---|---|---|
| ביקורת ארכיטקטורה נוכחית | יום אחד | תוכנית אינטגרציה בהתחשב בסטACK שלך |
| הגדרת VAPID ועובד שירות | 0.5 יום | עובד שירות מוכן עם טיפול ב-push |
| הטמעת מינוי (front+back) | 1–2 ימים | מינוי/ביטול מינוי עובד |
| הגדרת תרחישי שליחה | יום אחד | התראות מבוססות אירועים (הזמנה, רישום) |
| בדיקות וניטור | 0.5 יום | דוח משלוח + לוגי שגיאות |
לוח זמנים: 1 עד 3 ימים תלוי במורכבות התרחיש.
Web push למעורבות אתר הוא ערוץ מוכח; התראות push מוגדרות כראוי מגדילות ביקורים חוזרים ב-30% ואת ערך ההזמנה הממוצע ב-15%. לעיון נוסף: MDN Web Push API — תיעוד רשמי. התראות push בדפדפן נתמכות ב-Chrome, Firefox, Edge, Opera ו-Safari (מגרסה 16.4). שליחת התראות push דרך VAPID מבטיחה אימות ואבטחה.
מדריך זה מכסה את תהליך העבודה המלא של הטמעת התראות push, כולל טיפול בעובד שירות, הגדרת התראות push ב-Laravel והגדרת Push API.







