Глубокое погружение в 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.
- Тщательно контролируйте права доступа при подписке пользователей на теги.