Глубокое погружение в Real-time на 1С-Битрикс: Создаём «живые» интерфейсы с модулем Pull
В этой статье мы проведём детальный разбор модуля Pull — штатного инструмента 1С-Битрикс для создания интерактивных real-time приложений. Мы пройдём путь от теории и архитектуры до практических кейсов, разберём типовые ошибки и научимся строить производительные и безопасные решения, которые обновляются на лету без перезагрузки страницы.
Кирилл Новожилов
Автор
Содержание
Для кого эта статья?
Материал ориентирован на разработчиков Битрикс, которые уже владеют основами D7 и хотят освоить более сложную, но востребованную технологию. Если вы хотите, чтобы ваши интернет-магазины, CRM-системы и порталы реагировали на события мгновенно, — эта статья для вас.
Что вы узнаете?
- Архитектуру модуля 
pullи как он работает «под капотом». - Разницу между WebSocket и Long Polling и как Битрикс управляет транспортами.
 - Как отправлять адресные (приватные) и канальные (публичные) сообщения с сервера.
 - Как на клиенте подписываться на события и обновлять UI с помощью 
pull.client. - Как избежать распространенных ошибок и писать безопасный, масштабируемый код.
 
Часть 1: Теория и архитектура
Прежде чем писать код, важно понять, как устроен механизм. Модуль pull — это не просто API, а целая инфраструктура, обеспечивающая двустороннюю связь между сервером и клиентом.
Как это работает: общая схема
Весь процесс можно разбить на несколько ключевых этапов:
- Инициация события на сервере (PHP): В вашем PHP-коде происходит некое бизнес-событие (например, создан новый заказ). Вы вызываете метод модуля 
pull, чтобы отправить сообщение. - Запись в очередь: Сообщение не отправляется клиенту напрямую из PHP. Вместо этого оно помещается в специальную очередь в базе данных. Это делает отправку очень быстрой и неблокирующей для основного потока выполнения.
 - Push-сервер: Отдельный процесс (сервер), написанный на NodeJS или реализованный через модуль nginx, постоянно опрашивает эту очередь. Как только в ней появляется новое сообщение, он забирает его.
 - Доставка клиенту: Push-сервер находит всех клиентов, которые должны получить это сообщение, и отправляет им данные по установленному соединению (WebSocket или Long Polling).
 - Обработка на клиенте (JS): Клиентский JavaScript (
pull.client) получает событие и выполняет ваш callback-код — например, обновляет DOM, показывает уведомление или запрашивает новые данные по AJAX. 
Транспорты: 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 включён и настроен.
Настройка окружения
- Включить модуль: Убедитесь, что модуль 
Push and Pull(pull) установлен и активен в админ-панели (/bitrix/admin/module_admin.php). - Настроить транспорт: Необходимо настроить сервер очередей. При наличии активной лицензии вы можете использовать облачный сервис «1С-Битрикс». Альтернативно, можно настроить локальный Push-сервер. Детальная настройка сервера выходит за рамки данной статьи, подробную инструкцию вы найдете в официальной документации.
 - Подключить 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) работают только для авторизованных пользователей. Чтобы отправлять события всем посетителям, включая гостей, мы будем использовать специальный механизм, описанный в официальной документации:
- Инициализация гостя: На событии 
OnPrologмы проверяем, является ли пользователь гостем, и если да, то определяем для него константуPULL_USER_IDс отрицательным значением. Это "регистрирует" гостя в системе PULL на время его сессии. - Общий канал: События отправляются в специальный общий канал 
\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-событиями для всех посетителей сайта:
- Сервер (
init.php): "Легализует" гостя в системе PULL. - Сервер (Обработчик): При оплате заказа отправляет событие в общий канал.
 - Клиент: Получает событие и динамически обновляет DOM, независимо от статуса авторизации.
 
Полный исходный код компонента, готовый к установке и тестированию, вы можете найти в нашем 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',
    ]
]);
    
Почему это плохо?
- Безопасность: Вы рискуете передать на клиент приватные данные, которые не должны там оказаться. PULL-сообщения можно перехватить.
 - Связанность (Coupling): Бэкенд начинает отвечать за вёрстку, что нарушает разделение ответственности. Если дизайн изменится, придётся править PHP-код.
 - Производительность: Большой объём данных в сообщении увеличивает нагрузку на канал.
 
Хорошо:
        // Отправляем только 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: Документация и дальнейшее изучение
- Документация по модулю Push & Pull (DevDocs) — официальное руководство по настройке.
 - Класс 
\Bitrix\Pull\Event(API Docs) — описание методов для адресной отправки. - События модуля 
sale— список событий, на которые можно вешать свои обработчики. 
Заключение
Модуль pull — это мощный и гибкий инструмент, который превращает стандартный сайт на Битрикс в современное интерактивное веб-приложение. Он открывает двери для реализации самых разных сценариев: от простых уведомлений до сложных систем совместной работы.
Ключевые выводы:
- Всегда разделяйте логику на серверную (отправка событий) и клиентскую (обработка и обновление UI).
 - Используйте адресную отправку (
PullEvent::add) для приватных данных и теги (CPullWatch) для публичных каналов. - Передавайте в сообщениях только идентификаторы, а полные данные подгружайте по AJAX/REST.
 - Тщательно контролируйте права доступа при подписке пользователей на теги.