Туториал 26.08.2025 15 мин чтения Middle

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

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

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

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

Автор

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

Материал ориентирован на разработчиков Битрикс, которые уже владеют основами 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.
  • Тщательно контролируйте права доступа при подписке пользователей на теги.

Теги: