Возможности Как это работает Тарифы Документация Cookbook Партнёрам Личный кабинет
Основы
🔔

Уведомления в реальном времени

8 мин чтения Просто

Push-уведомления пользователям без перезагрузки страницы. Разные типы, звуки, счётчик непрочитанных.

JavaScript PHP Vue

📋 Что получится

  • Мгновенные уведомления в браузере
  • Разные типы: успех, ошибка, предупреждение
  • Звуковые оповещения (опционально)
  • Счётчик непрочитанных
Любой бэкенд: Примеры ниже на PHP, но вы можете использовать любой язык — Node.js, Python, Go, Ruby, Java и др. Pushler.ru предоставляет простой REST API для отправки событий.

🏗️ Архитектура

Событие
заказ, комментарий...
🖥️
Ваш сервер
PHP / Node.js / etc
Pushler.ru
WebSocket
🌐
Браузер
пользователя

Поток данных:

  1. На сервере происходит событие (новый заказ, комментарий и т.д.)
  2. Сервер отправляет сообщение в Pushler.ru
  3. Pushler.ru доставляет через WebSocket в браузер
  4. JavaScript показывает уведомление

💻 Код сервера

Ниже примеры на PHP с нашим SDK. Для других языков используйте REST API или официальные SDK.

Установка PHP SDK

Bash
composer require pushler/php-sdk

Сервис уведомлений

PHP
<?php
// NotificationService.php

require_once 'vendor/autoload.php';

use PushlerRu\PushlerClient;

class NotificationService
{
    private PushlerClient $pushler;

    public function __construct(string $appKey, string $appSecret)
    {
        $this->pushler = new PushlerClient($appKey, $appSecret);
    }

    /**
     * Отправить уведомление пользователю
     */
    public function notify(int $userId, string $title, string $message, string $type = 'info'): void
    {
        $this->pushler->trigger(
            "private-user-{$userId}",
            'notification',
            [
                'id' => uniqid('notif_'),
                'title' => $title,
                'message' => $message,
                'type' => $type, // info, success, warning, error
                'timestamp' => date('c'),
            ]
        );
    }

    /**
     * Уведомление об успешном действии
     */
    public function success(int $userId, string $title, string $message): void
    {
        $this->notify($userId, $title, $message, 'success');
    }

    /**
     * Уведомление об ошибке
     */
    public function error(int $userId, string $title, string $message): void
    {
        $this->notify($userId, $title, $message, 'error');
    }

    /**
     * Предупреждение
     */
    public function warning(int $userId, string $title, string $message): void
    {
        $this->notify($userId, $title, $message, 'warning');
    }
}

// Создаём сервис
$notifications = new NotificationService(
    'key_ваш_ключ',
    'secret_ваш_секрет'
);

Использование при создании заказа

PHP
<?php
// create_order.php

require_once 'NotificationService.php';

// Получаем данные заказа из POST
$orderData = $_POST;
$userId = getCurrentUserId(); // Ваша функция авторизации

// Сохраняем заказ в БД
$pdo = getDbConnection();
$stmt = $pdo->prepare('
    INSERT INTO orders (user_id, total, manager_id, created_at) 
    VALUES (?, ?, ?, NOW())
');
$stmt->execute([$userId, $orderData['total'], $orderData['manager_id']]);
$orderId = $pdo->lastInsertId();

// Уведомляем клиента
$notifications->success(
    $userId,
    'Заказ создан! 🎉',
    "Заказ #{$orderId} успешно оформлен. Ожидайте подтверждения."
);

// Уведомляем менеджера
$notifications->notify(
    $orderData['manager_id'],
    'Новый заказ',
    "Поступил заказ #{$orderId} на сумму {$orderData['total']} ₽",
    'info'
);

echo json_encode(['order_id' => $orderId, 'status' => 'created']);

Endpoint авторизации канала

PHP
<?php
// auth.php — авторизация приватных каналов

require_once 'vendor/autoload.php';

use PushlerRu\PushlerClient;

$pushler = new PushlerClient('key_ваш_ключ', 'secret_ваш_секрет');

header('Content-Type: application/json');

$channelName = $_POST['channel_name'];
$socketId = $_POST['socket_id'];
$userId = getCurrentUserId();

// Проверяем авторизацию
if (!$userId) {
    http_response_code(401);
    echo json_encode(['error' => 'Unauthorized']);
    exit;
}

// Формат канала: private-user-{userId}
// Пользователь может подписаться только на свой канал
if (preg_match('/^private-user-(\d+)$/', $channelName, $matches)) {
    if ((int) $matches[1] !== $userId) {
        http_response_code(403);
        echo json_encode(['error' => 'Forbidden']);
        exit;
    }
}

// Генерируем авторизацию
$auth = $pushler->authorizeChannel($channelName, $socketId);
echo json_encode($auth);

🌐 Код клиента (JavaScript)

Подключение SDK

HTML
<!-- Подключение из CDN -->
<script src="https://cdn.pushler.ru/sdk/pushler.min.js"></script>

<!-- Или npm: npm install @pushler/js -->

Vanilla JavaScript

JavaScript
// Конфигурация
const USER_ID = 123; // ID текущего пользователя
const APP_KEY = 'key_ваш_ключ';

// Иконки для разных типов
const icons = {
    info: 'ℹ️',
    success: '✅',
    warning: '⚠️',
    error: '❌'
};

// Счётчик непрочитанных
let unreadCount = 0;

// Инициализация Pushler
const pushler = new PushlerClient({
    appKey: APP_KEY,
    authEndpoint: '/auth.php'
});

// Подписка на приватный канал пользователя
pushler.on('connected', () => {
    console.log('Подключено к Pushler');
    
    const channel = pushler.subscribe(`private-user-${USER_ID}`);

    channel.on('notification', (data) => {
        showNotification(data);
        playSound();
        updateBadge(++unreadCount);
    });
});

// Показать уведомление
function showNotification(data) {
    const container = document.getElementById('notifications');
    const notification = document.createElement('div');
    notification.className = `notification ${data.type}`;
    notification.innerHTML = `
        <span class="notification-icon">${icons[data.type] || icons.info}</span>
        <div class="notification-content">
            <div class="notification-title">${escapeHtml(data.title)}</div>
            <div class="notification-message">${escapeHtml(data.message)}</div>
        </div>
        <button class="notification-close" onclick="closeNotification(this)">×</button>
    `;

    container.appendChild(notification);

    // Автоматически скрыть через 5 секунд
    setTimeout(() => {
        closeNotification(notification.querySelector('.notification-close'));
    }, 5000);
}

// Закрыть уведомление
function closeNotification(button) {
    const notification = button.closest('.notification');
    notification.style.animation = 'slideOut 0.3s ease forwards';
    setTimeout(() => notification.remove(), 300);
}

// Обновить счётчик
function updateBadge(count) {
    const badge = document.getElementById('notification-count');
    if (count > 0) {
        badge.textContent = count > 99 ? '99+' : count;
        badge.style.display = 'inline-flex';
    } else {
        badge.style.display = 'none';
    }
}

// Воспроизвести звук
function playSound() {
    const audio = new Audio('/sounds/notification.mp3');
    audio.volume = 0.3;
    audio.play().catch(() => {}); // Игнорируем ошибку если звук заблокирован
}

// Экранирование HTML
function escapeHtml(text) {
    const div = document.createElement('div');
    div.textContent = text;
    return div.innerHTML;
}

Vue 3 компонент

Vue
<!-- NotificationToast.vue -->
<template>
  <Teleport to="body">
    <TransitionGroup
      name="notification"
      tag="div"
      class="notifications-container"
    >
      <div
        v-for="notification in notifications"
        :key="notification.id"
        :class="['notification', notification.type]"
      >
        <span class="icon">{{ icons[notification.type] }}</span>
        <div class="content">
          <div class="title">{{ notification.title }}</div>
          <div class="message">{{ notification.message }}</div>
        </div>
        <button v-on:click="remove(notification.id)" class="close">×</button>
      </div>
    </TransitionGroup>
  </Teleport>
</template>

<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import PushlerClient from '@pushler/js';

const props = defineProps({
  userId: { type: Number, required: true },
  appKey: { type: String, required: true }
});

const emit = defineEmits(['notification', 'unread-change']);

const notifications = ref([]);
const unreadCount = ref(0);

const icons = {
  info: 'ℹ️',
  success: '✅',
  warning: '⚠️',
  error: '❌'
};

let pushler = null;
let channel = null;

onMounted(() => {
  pushler = new PushlerClient({
    appKey: props.appKey,
    authEndpoint: '/auth.php'
  });

  pushler.on('connected', () => {
    channel = pushler.subscribe(`private-user-${props.userId}`);
    channel.on('notification', handleNotification);
  });
});

onUnmounted(() => {
  channel?.unsubscribe();
  pushler?.disconnect();
});

function handleNotification(data) {
  notifications.value.push(data);
  unreadCount.value++;
  
  emit('notification', data);
  emit('unread-change', unreadCount.value);

  // Автоудаление через 5 секунд
  setTimeout(() => remove(data.id), 5000);
}

function remove(id) {
  const index = notifications.value.findIndex(n => n.id === id);
  if (index > -1) {
    notifications.value.splice(index, 1);
  }
}
</script>

<style scoped>
.notifications-container {
  position: fixed;
  top: 20px;
  right: 20px;
  z-index: 9999;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.notification {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  padding: 16px;
  background: #1e293b;
  border-radius: 12px;
  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
  min-width: 300px;
  max-width: 400px;
}

.notification.success { border-left: 4px solid #22c55e; }
.notification.error { border-left: 4px solid #ef4444; }
.notification.warning { border-left: 4px solid #f59e0b; }
.notification.info { border-left: 4px solid #3b82f6; }

.icon { font-size: 1.5rem; }
.content { flex: 1; }
.title { font-weight: 600; color: #f1f5f9; }
.message { font-size: 0.9rem; color: #94a3b8; margin-top: 4px; }
.close { 
  background: none; 
  border: none; 
  color: #64748b; 
  font-size: 1.25rem; 
  cursor: pointer; 
}

.notification-enter-active { animation: slideIn 0.3s ease; }
.notification-leave-active { animation: slideOut 0.3s ease; }

@keyframes slideIn {
  from { transform: translateX(100%); opacity: 0; }
  to { transform: translateX(0); opacity: 1; }
}

@keyframes slideOut {
  from { transform: translateX(0); opacity: 1; }
  to { transform: translateX(100%); opacity: 0; }
}
</style>

⚠️ Типичные ошибки

1. Уведомление не приходит

JavaScript
// ❌ Неправильно — публичный канал, но называется как приватный
pushler.subscribe('user-123');

// ✅ Правильно — приватный канал с префиксом
pushler.subscribe('private-user-123');

2. Ошибка авторизации канала

PHP
// ❌ Неправильно — endpoint не проверяет права
$auth = $pushler->authorizeChannel($channelName, $socketId);
echo json_encode($auth);

// ✅ Правильно — проверяем, что пользователь авторизован
$userId = getCurrentUserId();
if (!$userId) {
    http_response_code(401);
    echo json_encode(['error' => 'Unauthorized']);
    exit;
}

// Проверяем, что канал принадлежит этому пользователю
if (preg_match('/^private-user-(\d+)$/', $channelName, $matches)) {
    if ((int) $matches[1] !== $userId) {
        http_response_code(403);
        echo json_encode(['error' => 'Forbidden']);
        exit;
    }
}

$auth = $pushler->authorizeChannel($channelName, $socketId);
echo json_encode($auth);

3. Уведомление показывается дважды

JavaScript
// ❌ Неправильно — подписка в цикле/при каждом рендере
useEffect(() => {
    channel.on('notification', handler); // Добавляется каждый раз!
});

// ✅ Правильно — подписка один раз + очистка
useEffect(() => {
    channel.on('notification', handler);
    return () => channel.off('notification', handler);
}, []); // Пустой массив зависимостей!

4. authEndpoint не указан

JavaScript
// ❌ Неправильно — нет authEndpoint
const pushler = new PushlerClient({ appKey: 'key_xxx' });
pushler.subscribe('private-user-123'); // Ошибка авторизации!

// ✅ Правильно — указываем endpoint
const pushler = new PushlerClient({
    appKey: 'key_xxx',
    authEndpoint: '/auth.php'
});

Готовы попробовать?

Создайте бесплатный аккаунт и начните интеграцию за пару минут