26.08.2025 15 мин чтения

Глубокое погружение в Real-time на 1С-Битрикс: Создаём «живые» интерфейсы с модулем Pull

В этой статье мы проведём детальный разбор модуля Pull — штатного инструмента 1С-Битрикс для создания интерактивных real-time приложений. Мы пройдём путь от теории и архитектуры до практических кейсов, разберём типовые ошибки и научимся строить производительные и безопасные решения, которые обновляются на лету без перезагрузки страницы.

Кирилл Новожилов

Кирилл Новожилов

Автор

Глубокое погружение в Real-time на 1С-Битрикс: Создаём «живые» интерфейсы с модулем Pull

Для кого эта статья?

Материал ориентирован на разработчиков Битрикс, которые уже владеют основами D7 и хотят освоить более сложную, но востребованную технологию. Если вы хотите, чтобы ваши интернет-магазины, CRM-системы и порталы реагировали на события мгновенно, — эта статья для вас.

Что вы узнаете?

  • Архитектуру модуля pull и как он работает «под капотом».
  • Разницу между WebSocket и Long Polling и как Битрикс управляет транспортами.
  • Как отправлять адресные (приватные) и канальные (публичные) сообщения с сервера.
  • Как на клиенте подписываться на события и обновлять UI с помощью pull.client.
  • Как избежать распространенных ошибок и писать безопасный, масштабируемый код.

Часть 1: Теория и архитектура

Прежде чем писать код, важно понять, как устроен механизм. Модуль pull — это не просто API, а целая инфраструктура, обеспечивающая двустороннюю связь между сервером и клиентом.

Как это работает: общая схема

Весь процесс можно разбить на несколько ключевых этапов:

  1. Инициация события на сервере (PHP): В вашем PHP-коде происходит некое бизнес-событие (например, создан новый заказ). Вы вызываете метод модуля pull, чтобы отправить сообщение.
  2. Запись в очередь: Сообщение не отправляется клиенту напрямую из PHP. Вместо этого оно помещается в специальную очередь в базе данных. Это делает отправку очень быстрой и неблокирующей для основного потока выполнения.
  3. Push-сервер: Отдельный процесс (сервер), написанный на NodeJS или реализованный через модуль nginx, постоянно опрашивает эту очередь. Как только в ней появляется новое сообщение, он забирает его.
  4. Доставка клиенту: Push-сервер находит всех клиентов, которые должны получить это сообщение, и отправляет им данные по установленному соединению (WebSocket или Long Polling).
  5. Обработка на клиенте (JS): Клиентский JavaScript (pull.client) получает событие и выполняет ваш callback-код — например, обновляет DOM, показывает уведомление или запрашивает новые данные по AJAX.

Схема взаимодействия push-сервера

Транспорты: WebSocket vs. Long Polling

Битрикс поддерживает два основных способа доставки сообщений:

  • WebSocket: Наиболее современный и эффективный метод. Устанавливается постоянное двустороннее соединение между клиентом и сервером. Сообщения доставляются почти мгновенно с минимальными накладными расходами. Это предпочтительный вариант.
  • Long Polling: Резервный механизм. Клиент отправляет AJAX-запрос на сервер, который сервер «удерживает» открытым, пока не появится новое событие. Как только событие произошло, сервер отвечает, клиент его обрабатывает и тут же отправляет новый «длинный» запрос. Этот метод создаёт большую нагрузку, но работает везде, даже при ограничениях сети или через прокси.

К счастью, разработчику не нужно управлять этим вручную. Битрикс автоматически пытается установить WebSocket-соединение и, если оно не удалось, переключается на Long Polling. Исключение составляет принудительное отключение Websocket в настройках модуля.

Ключевые классы и пространства имен

Серверная часть (PHP):

  • \Bitrix\Main\Loader::includeModule('pull'): Обязательное подключение модуля перед использованием.
  • \Bitrix\Pull\Event: Основной класс для отправки адресных (приватных) сообщений конкретным пользователям. Его ключевой метод — add(array|int $userIds, array $params).
  • \CPullWatch: Legacy-класс для работы с каналами (тегами). Позволяет подписывать пользователей на публичные каналы и отправлять сообщения всем подписчикам тега. Основные методы: Add($userId, $tag) и AddToStack($tag, $params).

Также можно использовать функции старого ядра. Под капотом они часто вызывают методы D7, но в некоторых случаях их использование может быть проще, так как они выполняют дополнительную подготовку данных, как, например, CPullWatch::AddToStack().

Клиентская часть (JavaScript):

  • \Bitrix\Main\UI\Extension::load('pull.client'): Подключение JS-расширения на странице.
  • BX.PULL: Глобальный JS-объект для работы с real-time событиями.
    • subscribe(options): Современный метод подписки на события.
    • extendWatch(tag): Подписка на публичный тег-канал.
    • addCustomEvent('onPullEvent-<moduleId>', callback): Устаревший способ, который не рекомендуется использовать в новых проектах.

Часть 2: Базовые примеры кода

Перейдём к реализации. Все примеры предполагают, что модуль pull включён и настроен.

Настройка окружения

  1. Включить модуль: Убедитесь, что модуль Push and Pull (pull) установлен и активен в админ-панели (/bitrix/admin/module_admin.php).
  2. Настроить транспорт: Необходимо настроить сервер очередей. При наличии активной лицензии вы можете использовать облачный сервис «1С-Битрикс». Альтернативно, можно настроить локальный Push-сервер. Детальная настройка сервера выходит за рамки данной статьи, подробную инструкцию вы найдете в официальной документации.
  3. Подключить JS-расширение: На всех страницах, где требуется real-time функциональность, подключите клиентскую библиотеку.
            // В шаблоне компонента или в header.php
    \Bitrix\Main\UI\Extension::load('pull.client');
    
        

Пример 1: Уведомление о смене статуса заказа (Адресная отправка)

Задача: Когда менеджер меняет статус заказа, пользователь, сделавший этот заказ, должен мгновенно увидеть изменение на странице своих заказов.

Решение Мы повесим обработчик на событие сохранения заказа OnSaleOrderSaved и будем отправлять адресное PULL-событие.
        // Файл: /local/php_interface/init.php
use Bitrix\Main\Event;
use Bitrix\Main\EventManager;
use Bitrix\Main\Loader;
use Bitrix\Pull\Event as PullEvent;
use Bitrix\Sale\Order;

EventManager::getInstance()->addEventHandler(
    'sale',
    'OnSaleOrderSaved',
    static function(Event $event): void {
        /** @var Order $order */
        $order = $event->getParameter('ENTITY');
        // Проверяем, что это не новый заказ и есть изменения
        if (!$order || $event->getParameter('IS_NEW') || !$order->isChanged()) {
            return;
        }

        // Нас интересует только изменение поля STATUS_ID
        if (!$order->isChanged('STATUS_ID')) {
            return;
        }
        
        $userId = (int)$order->getUserId();
        $orderId = (int)$order->getId();
        // Получаем символьный код статуса из справочника
        $status = \Bitrix\Sale\OrderStatus::GetList([
            'filter' => ['ID' => $order->getField('STATUS_ID')],
            'select' => ['NAME'],
            'limit' => 1
        ])->fetch();

        $orderStatus = 'N/A';
        if (!empty($status)) {
            $orderStatus = $status['NAME'];
        }

        // Отправляем событие только авторизованному пользователю
        if ($userId > 0 && Loader::includeModule('pull')) {
            PullEvent::add($userId, [ // Можно передать массив ID пользователей: [$userId, $adminId]
                'module_id' => 'sale', // Уникальный идентификатор вашего модуля
                'command'   => 'order_status_updated', // Название команды-события
                'params'    => [ // Полезная нагрузка
                    'id' => $orderId,
                    'status' => $orderStatus
                ],
            ]);
        }
    }
);

    

Клиентская часть (в шаблоне списка заказов):

        BX.ready(() => {
  // Используем современный метод подписки
  BX.PULL.subscribe({
    moduleId: 'sale',
    command: 'order_status_updated',
    callback: (data) => {
      // data.id и data.status придут из PHP
      console.log('Статус заказа обновлен:', data);
      
      const orderRow = document.querySelector(`[data-order-id="${data.id}"]`);
      if (orderRow) {
        const statusCell = orderRow.querySelector('[data-role="order-status"]');
        if (statusCell) {
          statusCell.textContent = data.status;
          // Добавим красивую подсветку на пару секунд
          statusCell.style.transition = 'background-color 0.3s';
          statusCell.style.backgroundColor = '#fffae9';
          setTimeout(() => {
            statusCell.style.backgroundColor = '';
          }, 2000);
        }
      }
    }
  });
});

    

Пример 2: Лента новых заказов для администраторов (Каналы/Теги)

Задача: Создать на дашборде администратора живую ленту, куда будут падать новые заказы сразу после их создания.

Решение Мы будем использовать теги. Всех администраторов при заходе на страницу дашборда мы подпишем на тег ADMIN_ORDER_LIST. При создании нового заказа мы будем отправлять событие в этот тег.

Шаг 1: Подписка администраторов на тег. Это нужно делать в коде компонента дашборда.

        // В class.php компонента дашборда
use Bitrix\Main\Loader;
use Bitrix\Main\Engine\CurrentUser;

class DashboardComponent extends \CBitrixComponent
{
    public function executeComponent()
    {
        // ... ваш код
        if (Loader::includeModule('pull') && CurrentUser::get()->isAdmin()) {
            // Подписываем текущего пользователя (админа) на тег
            \CPullWatch::Add(CurrentUser::get()->getId(), 'ADMIN_ORDER_LIST');
        }
        $this->includeComponentTemplate();
    }
}

    

Шаг 2: Отправка события при создании заказа.

        // Файл: /local/php_interface/init.php (дополняем обработчик)
EventManager::getInstance()->addEventHandler(
    'sale',
    'OnSaleOrderSaved',
    static function(Event $event): void {
        // ... код из предыдущего примера для обновления статуса
        
        // --- Добавляем логику для новых заказов ---
        if (!$event->getParameter('IS_NEW')) {
            return;
        }

        /** @var Order $order */
        $order = $event->getParameter('ENTITY');
        $orderId = (int)$order->getId();
        $orderSum = (float)$order->getPrice();
        
        if (Loader::includeModule('pull')) {
					  $user = \Bitrix\Main\UserTable::getById($order->getUserId())->fetchObject();
					
            // Отправляем событие в тег, на который подписаны администраторы
            \CPullWatch::AddToStack('ADMIN_ORDER_LIST', [
                'module_id' => 'sale',
                'command' => 'new_order_created',
                'params' => [
                    'id' => $orderId,
                    'sum' => $orderSum,
                    'currency' => $order->getCurrency(),
                    'user' => $user->getLastName() . ' ' . $user->getName(),
                ],
            ]);
        }
    }
);

    

Шаг 3: Клиентская часть на дашборде.

        BX.ready(() => {
  const listNode = document.querySelector('#live-orders-list');
  
  // 1. Уведомляем JS-библиотеку, что мы следим за этим тегом
  BX.PULL.extendWatch('ADMIN_ORDER_LIST');

  // 2. Подписываемся на команду
  BX.PULL.subscribe({
    moduleId: 'sale',
    command: 'new_order_created',
    callback: (data) => {
      console.log('Пришел новый заказ:', data);
      if (!listNode) return;
      
      const newOrderHtml = `
        <div class="order-item" style="animation: fadeIn 0.5s;">
            Новый заказ <a href="/bitrix/admin/sale_order_view.php?ID=${data.id}">#${data.id}</a>
            от ${data.user} на сумму ${data.sum} ${data.currency}.
        </div>
      `;
      
      listNode.insertAdjacentHTML('afterbegin', newOrderHtml);
    }
  });
});

    

Часть 3: Полноценный кейс: компонент «Лента покупок»

Теперь давайте разберём полноценный, готовый к использованию компонент bxmax:recent-purchases, который показывает ленту последних покупок в интернет-магазине в реальном времени.

Задача компонента

Компонент должен выводить список недавно купленных товаров и, как только происходит новая оплаченная покупка, мгновенно добавлять её в начало списка у всех пользователей (включая неавторизованных), которые видят этот компонент.

Ключевая особенность: события для гостей

Стандартные каналы (CPullWatch) работают только для авторизованных пользователей. Чтобы отправлять события всем посетителям, включая гостей, мы будем использовать специальный механизм, описанный в официальной документации:

  1. Инициализация гостя: На событии OnProlog мы проверяем, является ли пользователь гостем, и если да, то определяем для него константу PULL_USER_ID с отрицательным значением. Это "регистрирует" гостя в системе PULL на время его сессии.
  2. Общий канал: События отправляются в специальный общий канал \Bitrix\Pull\Event::SHARED_CHANNEL.

Структура файлов

Компонент имеет стандартную для Битрикс-компонентов структуру:

        /local/components/bxmax/recent-purchases/

├── class.php             # Основная логика
├── .description.php      # Описание для визуального редактора
└── templates/
    └── .default/
        ├── template.php  # HTML для отображения
        ├── style.css     # CSS для отображения
        └── script.js     # JS для отображения

    

Шаг 1: Инициализация гостевого PULL-доступа

Этот код необходимо добавить в файл /local/php_interface/init.php. Он будет выполняться на каждом хите. Основная магия происходит в обработчике onSaleOrderSaved.

        // /local/php_interface/init.php

use Bitrix\Main\EventManager;
use Bitrix\Sale\BasketItem;
use Bitrix\Sale\Order;
use Bitrix\Sale\Basket;

EventManager::getInstance()->addEventHandler(
    'main',
    'OnProlog',
    'initPullForGuests'
);

EventManager::getInstance()->addEventHandler(
    "sale",
    "OnSaleOrderSaved",
    "onSaleOrderSaved"
);

function initPullForGuests()
{
    global $USER;
    // Если пользователь авторизован или константа уже определена, ничего не делаем
    if ($USER->IsAuthorized() || defined('PULL_USER_ID')) {
        return;
    }
    // Определяем "виртуальный" ID для всех гостей.
    // Отрицательное значение - обязательно.
    define('PULL_USER_ID', -1);
}

function onSaleOrderSaved($event)
{
    $order = $event->getParameter('ENTITY');
    if (!$order instanceof Order) {
        return;
    }

    $basket = Basket::loadItemsForOrder($order);
    /** @var BasketItem $basketItem */
    foreach ($basket as $basketItem) {
        $productId = $basketItem->getProductId();

        $res = CIBlockElement::GetList(
            arFilter: ['ID' => $productId],
            arNavStartParams: ['nTopCount' => 1],
            arSelectFields: ['ID', 'NAME', 'DETAIL_PAGE_URL']
        );
        $product = $res->GetNext();

        if (!empty($product)) {
            \Bitrix\Pull\Event::add(\Bitrix\Pull\Event::SHARED_CHANNEL, [
                'module_id' => 'bxmax.recent-purchases',
                'command' => 'purchase.completed',
                'params' => [
                    'id' => $product['ID'],
                    'name' => $product['NAME'],
                    'url' => $product['DETAIL_PAGE_URL'],
                    'orderId' => $order->getId()
                ]
            ], \CPullChannel::TYPE_SHARED);
        }
    }
}

    

Шаг 2: Серверная логика: class.php

В классе компонента не нужно заботиться о подписке: executeComponent просто получает данные и подключает JS.

        // local/components/bxmax/recent-purchases/class.php

public function executeComponent()
{
    $this->arResult['ITEMS'] = $this->getRecentPurchases();
    // Просто подключаем JS-расширение
    \Bitrix\Main\UI\Extension::load(['pull.client']);
    $this->includeComponentTemplate();
}



    

Шаг 3: Клиентская часть: template.php

Клиентская часть практически не меняется. Метод BX.PULL.subscribe отлично работает и с событиями из общего канала. Главное — правильно указать moduleId и command.

        // local/components/bxmax/recent-purchases/templates/.default/script.js

BX.ready(function() {
    // extendWatch здесь НЕ НУЖЕН, так как мы работаем с общим каналом,
    // а не с подпиской на тег.
    
    // Подписываемся на конкретную команду от нашего модуля.
    // PULL-клиент сам поймет, что это событие из общего канала.
    BX.PULL.subscribe({
        moduleId: 'bxmax.recent-purchases',
        command: 'purchase.completed',
        callback: function(data) {
            if (data && data.name && data.url) {
                addNewPurchase(data);
            }
        }
    });
    
    function addNewPurchase(data) {
        // ... здесь код, который создаёт новый HTML-элемент ...
    }
});

    

Этот компонент демонстрирует элегантный способ работы с PULL-событиями для всех посетителей сайта:

  1. Сервер (init.php): "Легализует" гостя в системе PULL.
  2. Сервер (Обработчик): При оплате заказа отправляет событие в общий канал.
  3. Клиент: Получает событие и динамически обновляет DOM, независимо от статуса авторизации.

Полный исходный код компонента, готовый к установке и тестированию, вы можете найти в нашем GitHub-репозитории.

Ссылка на GitHub

Часть 4: Безопасность и антипаттерны

При работе с real-time важно помнить о безопасности и лучших практиках. Неправильный подход может привести к утечке данных или излишней нагрузке.

❌ Антипаттерн 1: Передача HTML или чувствительных данных в params

Плохо:

        PullEvent::add($userId, [
    'module_id' => 'sale',
    'command' => 'order_status_updated',
    'params' => [
        'id' => $orderId,
        // Так делать НЕЛЬЗЯ!
        'html' => "<div class='status'>Новый статус: <b>Оплачен</b></div>",
        'user_phone' => '+79991234567',
    ]
]);

    

Почему это плохо?

  1. Безопасность: Вы рискуете передать на клиент приватные данные, которые не должны там оказаться. PULL-сообщения можно перехватить.
  2. Связанность (Coupling): Бэкенд начинает отвечать за вёрстку, что нарушает разделение ответственности. Если дизайн изменится, придётся править PHP-код.
  3. Производительность: Большой объём данных в сообщении увеличивает нагрузку на канал.

Хорошо:

        // Отправляем только ID и необходимый минимум данных
PullEvent::add($userId, [
    'module_id' => 'sale',
    'command' => 'order_status_updated',
    'params' => [
        'id' => $orderId,
        'statusCode' => 'P', // Код статуса
    ]
]);

// На клиенте
BX.PULL.subscribe({ /* ... */ callback: (data) => {
    // Получаем недостающие данные (например, название статуса)
    // отдельным AJAX-запросом к защищенному API
    BX.ajax.runComponentAction('my:component', 'getStatusName', {
        mode: 'class',
        data: { code: data.statusCode }
    }).then((response) => {
        // ... и только потом обновляем интерфейс
        updateOrderStatus(data.id, response.data.name);
    });
}});

    

Вывод: Передавайте по PULL только сигнал и идентификаторы. Все необходимые данные клиент должен запрашивать дополнительно через защищённое API.

❌ Антипаттерн 2: Бездумная подписка на теги

Плохо:

        // В шаблоне компонента карточки товара
// Подписываем любого посетителя сайта на тег
if (Loader::includeModule('pull')) {
    \CPullWatch::Add(CurrentUser::get()->getId(), 'PRODUCT_PRICE_UPDATES_' . $productId);
}

    

Почему это плохо? Вы даёте любому пользователю доступ к каналу, который может использоваться для рассылки внутренней информации. Даже если сейчас по нему ходят только цены, завтра другой разработчик может по ошибке отправить туда данные о скидках для VIP-клиентов.

Хорошо:

        // В классе компонента
if ($this->userHasRightsToSeePrices() && Loader::includeModule('pull')) {
    \CPullWatch::Add(CurrentUser::get()->getId(), 'PRODUCT_PRICE_UPDATES_' . $productId);
}

// ... где-то в коде класса
private function userHasRightsToSeePrices(): bool
{
    // Здесь ваша логика проверки прав доступа
    // Например, входит ли пользователь в нужную группу
    return true; 
}

    

Вывод: Подписка пользователя на тег (CPullWatch::Add) — это операция, которая должна выполняться на сервере и только после проверки прав доступа.

Часть 5: Документация и дальнейшее изучение

Заключение

Модуль pull — это мощный и гибкий инструмент, который превращает стандартный сайт на Битрикс в современное интерактивное веб-приложение. Он открывает двери для реализации самых разных сценариев: от простых уведомлений до сложных систем совместной работы.

Ключевые выводы:

  • Всегда разделяйте логику на серверную (отправка событий) и клиентскую (обработка и обновление UI).
  • Используйте адресную отправку (PullEvent::add) для приватных данных и теги (CPullWatch) для публичных каналов.
  • Передавайте в сообщениях только идентификаторы, а полные данные подгружайте по AJAX/REST.
  • Тщательно контролируйте права доступа при подписке пользователей на теги.
Опубликовано 5 месяцев назад

Теги:

Похожие статьи

Мы используем файлы cookie для улучшения работы сайта. Продолжая использовать сайт, вы соглашаетесь с нашей политикой конфиденциальности.