Клиент Push and Pull получает команды сервера и передает их JavaScript-обработчикам. Обработчик может изменить текст, обновить счетчик или запросить данные без перезагрузки страницы. Для подписки на команды используйте BX.PULL.subscribe.
Настройте обработку команд на примере отчета. Сначала страница будет менять текст при получении команды, затем — читать сохраненное состояние отдельным HTTP-запросом. Повторное чтение позволит показать результат, даже если пользователь открыл страницу после завершения отчета или пропустил команду из-за разрыва связи.
Подключение и готовность клиента
Используйте готовую страницу получателя /local/pull-demo/index.php из статьи Обновление страницы по команде сервера. Если транспорт еще не настроен, выполните настройку модуля и сервера очередей.
В примерах кода используются:
-
идентификатор модуля
example.reports, -
команда
reportReady, -
идентификатор отчета
42вparams.reportId, -
текст для страницы в
params.text.
Эти значения совпадают с настройками получателя из статьи Обновление страницы по команде сервера.
В PHP-шаблоне после загрузки окружения Bitrix проверьте модуль и подключите расширение:
Для собственного JS-расширения можно указать pull.client в зависимостях.
На подготовленной странице используйте экземпляр BX.PULL. Выполните код после загрузки библиотеки и создания элементов страницы. Один клиент обслуживает обработчики разных блоков интерфейса.
На тестовой странице оставьте обработчик OnGetDependentModule из статьи Обновление страницы по команде сервера и подключение /bitrix/footer.php. Условия автоматического запуска клиента приведены в разделе Как запускается клиент.
Подписка на команду и маршрутизация
Начните с обработчика одной команды. Если нужно принимать несколько команд модуля, используйте общий обработчик или разнесите команды по методам класса. Выберите один вариант. Если подключить все примеры одновременно, одну команду будут обрабатывать несколько функций.
Обработать одну команду
На странице /local/pull-demo/index.php из статьи Обновление страницы по команде сервера замените разметку результата и JS-обработчик следующим фрагментом. Сохраните PHP-подключение окружения и pull.client. Отправителем остается /local/pull-demo/send.php из той же статьи.
<p id="report-status" aria-live="polite">Ожидание команды для отчета 42.</p>
<button id="report-stop" type="button">Прекратить обработку</button>
<script>
BX.ready(function () {
const output = document.getElementById('report-status');
const stopButton = document.getElementById('report-stop');
if (!BX.PULL || typeof BX.PULL.subscribe !== 'function')
{
output.textContent = 'Клиент Push and Pull недоступен.';
stopButton.disabled = true;
return;
}
const unsubscribe = BX.PULL.subscribe({
moduleId: 'example.reports',
command: 'reportReady',
callback: function (params, extra, command) {
if (!params || params.reportId !== 42 || typeof params.text !== 'string')
{
return;
}
output.textContent = params.text;
}
});
stopButton.addEventListener('click', function () {
unsubscribe();
stopButton.disabled = true;
output.textContent = 'Обработка команд прекращена.';
}, { once: true });
});
</script>
Ожидаемый результат: после отправки команды страница покажет текст из params.text. Нажмите Прекратить обработку и отправьте команду еще раз. Текст не должен измениться.
Число 42 — идентификатор тестового отчета. В приложении передавайте идентификатор открытого объекта.
Для регистрации обработчика передайте в BX.PULL.subscribe объект со следующими параметрами:
-
moduleId— строка, соответствующая серверномуmodule_id, -
command— строка с именем выбранной команды, -
callback— функция обработки, -
type— тип подписки, по умолчаниюBX.PullClient.SubscriptionType.Server.
Для серверных команд параметр type можно не задавать. Типы Client и Online не заменяют серверную подписку в этом примере.
При подписке на одну команду обработчик принимает аргументы params, extra, command. Данные отчета находятся непосредственно в params, поэтому читайте params.reportId. Аргумент extra содержит служебные сведения о доставке, command — имя команды.
Четвертым аргументом клиент передает объект {type, moduleId} с типом подписки и идентификатором модуля. В примере он не нужен. Не используйте служебное время из extra как версию отчета.
Метод BX.PULL.subscribe задает, какая функция обработает полученную команду. Получателя и его права нужно проверить на сервере до отправки. Подписка в JavaScript не создает серверное наблюдение по тегу. Для адресной отправки пользователю тег не нужен. Наблюдение за объектом описано в статье Подписки по тегам.
Обработать несколько команд
Опустите поле command, чтобы обрабатывать команды выбранного модуля. Функция callback получит объект data с полями command, params, extra. Вторым аргументом клиент передаст объект {type, moduleId}.
Пример. Обработчик выводит в консоль результат для двух команд модуля отчетов:
const unsubscribe = BX.PULL.subscribe({
moduleId: 'example.reports',
callback: function (data) {
if (!data.params || data.params.reportId !== 42)
{
return;
}
switch (data.command)
{
case 'reportReady':
console.log('Отчет готов', data.params);
break;
case 'reportFailed':
console.log('Ошибка формирования отчета', data.params);
break;
}
}
});
Команда reportFailed добавлена для примера. Отправляйте ее из приложения при ошибке формирования отчета. Модуль pull только передает команду и не формирует сам отчет. Неизвестные команды обработчик пропускает.
Разнести команды по методам класса
Создайте класс с обработчиками команд. В методе getModuleId() укажите модуль, а в getMap() сопоставьте имена команд и функции. Вызов bind(this) сохранит доступ к полям экземпляра класса:
class ReportCommands
{
constructor(reportId, onChange)
{
this.reportId = reportId;
this.onChange = onChange;
}
getModuleId()
{
return 'example.reports';
}
getMap()
{
return {
reportReady: this.handleChange.bind(this),
reportFailed: this.handleChange.bind(this)
};
}
handleChange(params, extra, command)
{
if (params && params.reportId === this.reportId)
{
this.onChange(params, extra, command);
}
}
}
const unsubscribe = BX.PULL.subscribe(new ReportCommands(42, function (params) {
console.log('Состояние отчета изменилось', params);
}));
Класс может обойтись без getMap(): для команды reportReady клиент найдет метод handleReportReady(params, extra, command). Метод getModuleId() остается обязательным. Если getSubscriptionType() не задан, используется тип подписки Server.
Если getMap() есть, клиент сначала ищет обработчик команды в ней. Запись может содержать функцию или строку с именем метода класса. Найденный обработчик имеет приоритет перед методом handleReportReady(). Если карта не дала обработчик, клиент ищет метод по имени команды.
Все показанные браузерные формы подписки возвращают функцию для снятия обработчика. При переносе класса в мобильный веб-клиент учитывайте ограничение из раздела Браузерный и мобильный клиенты.
Первоначальная загрузка данных и подписка
Если сначала прочитать отчет, а затем зарегистрировать обработчик, изменение между этими действиями может остаться незамеченным. Регистрация до чтения позволяет обработчику принять команду во время запроса, но еще не подтверждает готовность транспорта. Поэтому используйте следующий порядок:
-
Зарегистрируйте обработчик команды.
-
Прочитайте актуальное состояние отчета через HTTP API приложения.
-
При команде запрашивайте состояние повторно. Если команда пришла во время чтения, запланируйте еще одно чтение после завершения текущего.
-
Выполняйте контрольное чтение при возврате к странице и периодически, пока интерфейс открыт. Это позволяет обнаружить изменение, уведомление о котором не дошло.
Функция mountReportView подписывает интерфейс на команды, читает отчет и возвращает функцию destroy для завершения наблюдения. Сохраните реализацию mountReportView в /local/pull-demo/report-view.js. Передайте объект с четырьмя параметрами:
-
reportId— числовой идентификатор открытого отчета, -
output— DOM-элемент для текста отчета, -
status— отдельный DOM-элемент для результата проверки, -
readReport— функция чтения данных приложения.
При закрытии интерфейса вызовите destroy(). Функция снимет подписки, остановит таймер и отменит текущий запрос.
Функция readReport читает данные приложения по следующим правилам:
-
аргументы —
reportIdиAbortSignalдля отмены запроса. -
результат —
Promiseс объектом{ reportId: 42, revision: 7, text: 'Отчет 42 готов.' }. -
revision— версия объекта типаnumber, безопасное целое JavaScript от0доNumber.MAX_SAFE_INTEGER. Сервер увеличивает версию при каждом изменении отображаемого состояния. -
ошибка — отклоненный
Promise, в том числе при отсутствии доступа или удалении отчета.
Готовая реализация чтения приведена в разделе Учебный пример с сохраненным состоянием.
На сервере проверяйте права текущего пользователя при каждом чтении. Возвращайте данные и версию одного состояния отчета. После изменения объекта не возвращайте из кеша его прежнее состояние. Поле revision задает приложение, оно не входит в API Push and Pull.
function mountReportView({ reportId, output, status, readReport })
{
// Состояние интерфейса и очереди запросов
let disposed = false;
let running = false;
let pending = false;
let revision = -1;
let request = null;
async function refresh()
{
if (disposed)
{
return;
}
// Если запрос уже идет, запланируйте чтение после его завершения
pending = true;
if (running)
{
return;
}
running = true;
try
{
// Команды во время запроса объединяются в повторное чтение
while (pending && !disposed)
{
pending = false;
// Ограничьте время чтения и сохраните возможность отмены
request = new AbortController();
const timeout = setTimeout(() => request.abort(), 10000);
try
{
const data = await readReport(reportId, request.signal);
// Не обновляйте интерфейс, закрытый во время запроса
if (disposed)
{
return;
}
// Проверьте объект и формат ответа перед выводом
if (!data || data.reportId !== reportId
|| !Number.isSafeInteger(data.revision) || data.revision < 0
|| typeof data.text !== 'string')
{
throw new Error('Некорректный ответ чтения отчета');
}
// Не заменяйте показанные данные более старым состоянием
if (data.revision >= revision)
{
revision = data.revision;
output.textContent = data.text;
status.textContent = 'Состояние проверено.';
}
else
{
status.textContent = 'Получена старая версия. Нужна повторная проверка.';
}
}
catch (error)
{
if (!disposed)
{
// Очистите данные при ошибке, в том числе при отказе в доступе
output.textContent = '';
status.textContent = 'Не удалось прочитать отчет. Проверка повторится.';
}
}
finally
{
clearTimeout(timeout);
request = null;
}
}
}
finally
{
running = false;
}
}
// Сначала подписка, затем первоначальное чтение
const unsubscribe = BX.PULL.subscribe({
moduleId: 'example.reports',
callback: function (data) {
if ((data.command === 'reportReady' || data.command === 'reportFailed')
&& data.params && data.params.reportId === reportId)
{
refresh();
}
}
});
// Перечитайте отчет при первом подключении и восстановлении связи
const unsubscribeStatus = BX.PULL.subscribe({
type: BX.PullClient.SubscriptionType.Status,
callback: function (data) {
if (data.status === 'online')
{
refresh();
}
}
});
function onVisible()
{
if (document.visibilityState === 'visible')
{
refresh();
}
}
// Контрольное чтение обнаружит изменения даже без pull-команды
window.addEventListener('online', refresh);
document.addEventListener('visibilitychange', onVisible);
const timer = setInterval(refresh, 30000);
refresh();
return function destroy()
{
if (disposed)
{
return;
}
// Освободите подписки и таймеры, затем отмените незавершенное чтение
disposed = true;
unsubscribe();
unsubscribeStatus();
clearInterval(timer);
window.removeEventListener('online', refresh);
document.removeEventListener('visibilitychange', onVisible);
if (request)
{
request.abort();
}
};
}
Подключите файл с mountReportView до вызова функции. Вызывайте ее после загрузки клиента и создания элементов страницы. Сохраните возвращенную функцию destroy, чтобы завершить наблюдение при закрытии интерфейса.
В примере контрольное чтение повторяется каждые 30 секунд, а запрос ограничен 10 секундами. Функция readReport должна учитывать AbortSignal и отклонять Promise при отмене запроса.
Эти интервалы не определяют задержку доставки Push and Pull. Подберите частоту контрольных запросов с учетом нагрузки на приложение. В фоновой вкладке браузер может замедлять таймеры.
Повторные команды и устаревшие данные
Обрабатывайте повторную команду так, чтобы она не меняла результат. Например, присваивание output.textContent = params.text оставит тот же текст. Увеличение суммы или добавление строки без проверки идентификатора повторно изменит данные. Не запускайте формирование отчета из обработчика команды о его готовности.
В функции mountReportView команда запускает чтение отчета. Флаг running не позволяет одному экземпляру интерфейса отправлять параллельные запросы. Если команда пришла во время запроса, флаг pending запланирует еще одно чтение. Несколько команд за время запроса приведут к одному повторному чтению.
Проверка revision не позволяет заменить показанную версию более старой. Если данные обновляют несколько независимых запросов, проверяйте версию во всех ответах по одному правилу для конкретного объекта. Номер HTTP-запроса показывает порядок запуска, но не подтверждает актуальность полученных данных.
Выводите полученные строки через textContent. Условие с reportId определяет, к какому отчету относится команда, но не заменяет проверку доступа на сервере.
При ошибке чтения функция очищает текст отчета. Так интерфейс не продолжит показывать данные после возможного отзыва прав.
В учебном примере все ошибки чтения приводят к повторной проверке, в том числе HTTP 401, 403 и 404. Функция readReport включает HTTP-статус только в текст ошибки, а mountReportView не различает причины отказа. Автоматическое завершение наблюдения при окончательном отказе в примере не реализовано.
Перед переносом в приложение добавьте обработку ошибок по правилам своего API. Передавайте HTTP-статус отдельным полем ошибки, например error.status. Для временной сетевой ошибки оставьте повторное чтение. При ответе, который приложение считает окончательным отказом, вызовите destroy(), чтобы снять подписки, остановить таймер и отменить запрос. Не определяйте причину отказа по тексту сообщения об ошибке.
Если передаете в команде все данные отчета, согласуйте их формат и версию между PHP и JavaScript. Критерии выбора между готовыми данными и идентификатором для повторного чтения приведены в статье Отправка команд из PHP.
Жизненный цикл обработчиков
Создавайте подписку для конкретного экземпляра интерфейса и вызывайте сохраненную функцию отписки при его закрытии.
В функции mountReportView вызов destroy снимает подписки на команды и состояние соединения, удаляет обработчики событий страницы, останавливает таймер и отменяет запрос. Флаг disposed не дает обновить закрытый интерфейс, если ответ пришел после его закрытия. Повторный вызов destroy безопасен.
Перед повторным открытием уничтожьте предыдущий экземпляр, затем создайте новый. Не регистрируйте обработчик при каждой перерисовке. При смене пользователя завершите прежнее наблюдение и заново выполните подготовку страницы для новой учетной записи.
Снятие JS-обработчика не прекращает серверное наблюдение по тегу. Обработчик определяет реакцию интерфейса, а наблюдение — состав получателей команды. Серверные операции описаны в статье Подписки по тегам. Работа компонента с кешем — в статье Обновление компонента после фоновой операции.
Учебный пример с сохраненным состоянием
Продолжите работу на подготовленной установке из раздела Подключение и готовность клиента. Сохраните функцию mountReportView в /local/pull-demo/report-view.js, затем создайте два файла ниже. Исключите /local/pull-demo/* из кеширования HTML, чтобы страницы получали данные текущей сессии.
Учебный отчет хранится в серверной сессии отдельно для текущего пользователя. Используйте две вкладки в одном профиле браузера под одной учетной записью. Данные сохраняются между запросами и перезагрузками страницы, пока действует сессия.
Пример не создает постоянное хранилище отчетов и не запускает фоновое задание. В приложении используйте свое хранилище с проверкой прав. Сохраните формат ответа на запрос чтения и поле версии объекта.
Создать HTTP-обработчик чтения и сохранения отчета
Создайте /local/pull-demo/report-state.php. Запрос GET читает отчет 42, а POST увеличивает его версию и при выборе отправки посылает команду. Код определяет владельца по текущему авторизованному пользователю.
Получите объект сессии через Application::getInstance()->getSession(). Используйте методы объекта сессии:
-
has($key)— проверить наличие записи, -
get($key)— прочитать запись, -
set($key, $state)— изменить данные, -
save()— сохранить и освободить сессию перед отправкой команды.
Ключ $key включает идентификатор текущего пользователя, переменная $state содержит состояние отчета. Передайте строку JSON в CMain::FinalActions(), чтобы отправить HTTP-ответ и штатно завершить приложение:
<?php
use Bitrix\Main\Application;
use Bitrix\Main\Loader;
use Bitrix\Main\Web\Json;
require $_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/prolog_before.php';
// Верните JSON без кеширования данных текущей сессии
header('Content-Type: application/json; charset=UTF-8');
header('Cache-Control: no-store');
$method = $_SERVER['REQUEST_METHOD'];
$result = [];
// Проверьте пользователя, параметры запроса и CSRF-токен перед изменением
if (!$USER->IsAuthorized())
{
http_response_code(401);
$result = ['error' => 'Требуется авторизация.'];
}
elseif (!in_array($method, ['GET', 'POST'], true))
{
http_response_code(405);
$result = ['error' => 'Используйте GET или POST.'];
}
elseif (($_GET['reportId'] ?? '') !== '42')
{
http_response_code(404);
$result = ['error' => 'Учебный отчет не найден.'];
}
elseif ($method === 'POST' && !check_bitrix_sessid())
{
http_response_code(403);
$result = ['error' => 'Проверка сессии не пройдена.'];
}
elseif ($method === 'POST' && !in_array($_POST['notify'] ?? '', ['0', '1'], true))
{
http_response_code(400);
$result = ['error' => 'Укажите notify: 0 или 1.'];
}
elseif (!Loader::includeModule('pull'))
{
http_response_code(503);
$result = ['error' => 'Модуль pull недоступен.'];
}
else
{
// Храните учебный отчет отдельно для текущего пользователя
$userId = (int)$USER->GetID();
$session = Application::getInstance()->getSession();
$key = 'pull_demo_report_42_user_' . $userId;
$state = $session->has($key) ? $session->get($key) : [
'reportId' => 42,
'revision' => 0,
'text' => 'Отчет 42 еще не изменяли.',
];
// Чтение не меняет отчет, POST создает следующее состояние
if ($method === 'POST')
{
$state['revision'] += 1;
$state['text'] = 'Отчет 42: сохранена версия ' . $state['revision'] . '.';
$session->set($key, $state);
}
// Сохраните сессию и освободите ее перед отправкой команды
$session->save();
$result = $state;
$result['accepted'] = null;
// Отправьте команду только после сохранения и по выбору пользователя
if ($method === 'POST' && $_POST['notify'] === '1')
{
$result['accepted'] = \CPullStack::AddByUser($userId, [
'module_id' => 'example.reports',
'command' => 'reportReady',
'params' => $state,
]) === true;
}
}
// Отправьте ответ и выполните завершающие действия приложения
\CMain::FinalActions(Json::encode($result));
У этого учебного отчета владелец — текущий пользователь в текущей сессии. Отчет другой сессии недоступен через этот обработчик. В рабочем приложении проверяйте право на объект при каждом чтении и изменении. Хранение в сессии не подходит для обмена состоянием между устройствами и фоновой задачей.
Ответ содержит reportId, revision, text и поле accepted с результатом добавления команды:
-
null— отправку не запрашивали, -
false— модуль не принял команду, -
true— модуль принял команду, но доставка еще не подтверждена.
Ошибка отправки не отменяет сохраненное состояние. Запрос GET не увеличивает версию и не запускает операцию заново. Не повторяйте POST автоматически при сетевом сбое. Каждый такой запрос создает следующую версию учебного отчета.
Подключить чтение и управление интерфейсом
Создайте /local/pull-demo/report.php. Кнопки на странице будут сохранять отчет с отправкой команды или без нее. Для этой проверки файл send.php из статьи Обновление страницы по команде сервера больше не нужен.
<?php
use Bitrix\Main\Loader;
use Bitrix\Main\UI\Extension;
require $_SERVER['DOCUMENT_ROOT'] . '/bitrix/header.php';
$APPLICATION->SetTitle('Сохраненное состояние и Push and Pull');
if (!$USER->IsAuthorized())
{
echo '<p>Войдите на сайт и откройте страницу повторно.</p>';
}
elseif (!Loader::includeModule('pull'))
{
echo '<p>Модуль pull недоступен.</p>';
}
else
{
Extension::load('pull.client');
?>
<p id="report-output" aria-live="polite"></p>
<p id="report-check"></p>
<p id="report-save-result" aria-live="polite"></p>
<button id="report-save" type="button">Сохранить и отправить команду</button>
<button id="report-save-silent" type="button">Сохранить без команды</button>
<button id="report-close" type="button">Закрыть наблюдение</button>
<button id="report-open" type="button" disabled>Открыть наблюдение</button>
<!-- Подключите функцию наблюдения до ее первого вызова -->
<script src="/local/pull-demo/report-view.js"></script>
<script>
BX.ready(function () {
const output = document.getElementById('report-output');
const status = document.getElementById('report-check');
const saveResult = document.getElementById('report-save-result');
const saveButton = document.getElementById('report-save');
const silentButton = document.getElementById('report-save-silent');
const closeButton = document.getElementById('report-close');
const openButton = document.getElementById('report-open');
if (!BX.PULL || typeof BX.PULL.subscribe !== 'function')
{
status.textContent = 'Клиент Push and Pull недоступен.';
[saveButton, silentButton, closeButton, openButton].forEach(button => {
button.disabled = true;
});
return;
}
// Читайте сохраненное состояние без кеша с поддержкой отмены
async function readReport(reportId, signal)
{
const response = await fetch('/local/pull-demo/report-state.php?reportId=' + reportId, {
credentials: 'same-origin',
cache: 'no-store',
signal: signal
});
if (!response.ok)
{
throw new Error('Ошибка чтения: HTTP ' + response.status);
}
return response.json();
}
// Сохраните функцию закрытия и не создавайте повторную подписку
let destroy = null;
function openView()
{
if (destroy)
{
return;
}
destroy = mountReportView({ reportId: 42, output, status, readReport });
openButton.disabled = true;
closeButton.disabled = false;
}
// При закрытии остановите наблюдение и очистите показанные данные
closeButton.addEventListener('click', function () {
if (destroy)
{
destroy();
destroy = null;
}
output.textContent = '';
status.textContent = 'Наблюдение закрыто.';
closeButton.disabled = true;
openButton.disabled = false;
});
openButton.addEventListener('click', openView);
// Сохраняйте отчет с командой или без нее, не повторяя POST автоматически
async function saveReport(notify)
{
saveButton.disabled = true;
silentButton.disabled = true;
try
{
const response = await fetch('/local/pull-demo/report-state.php?reportId=42', {
method: 'POST',
credentials: 'same-origin',
body: new URLSearchParams({
sessid: BX.bitrix_sessid(),
notify: notify ? '1' : '0'
})
});
const result = await response.json();
if (!response.ok)
{
throw new Error(result.error || 'Ошибка сохранения');
}
// Ответ сохранения меняет только строку диагностики
const message = result.accepted === null ? 'Команду не отправляли.'
: result.accepted ? 'Команда принята.' : 'Команда не принята.';
saveResult.textContent = 'Сохранена версия ' + result.revision + '. ' + message;
}
catch (error)
{
saveResult.textContent = 'Не удалось подтвердить сохранение. Проверьте состояние чтением.';
}
finally
{
saveButton.disabled = false;
silentButton.disabled = false;
}
}
// Подключите кнопки и выполните первоначальное чтение
saveButton.addEventListener('click', () => saveReport(true));
silentButton.addEventListener('click', () => saveReport(false));
openView();
});
</script>
<?php
}
require $_SERVER['DOCUMENT_ROOT'] . '/bitrix/footer.php';
После ответа на запрос сохранения код меняет только строку report-save-result с результатом отправки. Текст отчета обновляет функция mountReportView после GET-запроса. Поэтому страница получит сохраненное состояние и без новой команды.
Проверить восстановление и повторное открытие
-
Откройте
report.phpв двух вкладках одной сессии. Обе должны показать начальное или ранее сохраненное состояние. -
В первой вкладке нажмите Сохранить и отправить команду. Убедитесь, что обе вкладки показывают новую версию. Чтобы проверить именно доставку команды, поставьте точку останова в
callbackподпискиexample.reportsна условииdata.command === 'reportReady'. Одно изменение текста не доказывает доставку, поскольку его могло вызвать контрольное чтение. -
Нажмите Сохранить без команды. Состояние на сервере изменится без уведомления. Вторая вкладка должна получить новую версию при контрольном чтении по таймеру или возвращении к вкладке. Так можно проверить пропущенную команду без остановки сервера очередей.
-
Закройте наблюдение во второй вкладке и сохраните еще одну версию из первой. Закрытый интерфейс не должен обновляться. Нажмите Открыть наблюдение. Первоначальное чтение должно получить сохраненную версию без повторной отправки команды.
-
Отключите сеть только у вкладки получателя через инструменты разработчика. Из другой вкладки сохраните состояние без команды, затем восстановите сеть получателя. После успешного чтения он должен показать новую версию.
-
Отдельно проверьте разрыв pull-соединения. Подтвердите закрытие соединения на вкладке Network или переход клиента в
offline. Переключение сетевого режима вкладки само по себе не доказывает разрыв. -
Повторите закрытие и открытие наблюдения. На вкладке Network не должно появляться дополнительных повторяющихся запросов от закрытых экземпляров. Перезагрузите страницу. Сохраненная версия должна остаться доступной в той же сессии.
-
Отправьте POST без
sessid. Ожидается HTTP403без изменения версии. Запрос сreportId=43должен получить404. -
Включите замедление сети, начните чтение и закройте наблюдение. Ответ, который придет после закрытия, не должен изменить элементы страницы.
После проверки удалите /local/pull-demo/report-view.js, /local/pull-demo/report-state.php и /local/pull-demo/report.php, если они больше не нужны.
Переподключение и сверка состояния
При открытии страницы клиент подключается к серверу очередей. После разрыва связи проверьте состояние соединения и заново прочитайте данные отчета.
Как запускается клиент
На обычной странице публичной части расширение создает экземпляр клиента. Метод CPullOptions::OnEpilog() добавляет его запуск при загрузке окна. Для запуска нужны ненулевой идентификатор пользователя и разрешение использовать Pull в текущем разделе. Константа BX_PULL_SKIP_INIT и параметр запроса AJAX_CALL = 'Y' запрещают автоматический запуск.
Обработчик OnGetDependentModule объявляет использование Pull в публичной части, а /bitrix/footer.php завершает обработку страницы. Одного вызова Extension::load() в произвольном AJAX-фрагменте недостаточно для автоматического запуска клиента.
Восстановить данные после подключения
После восстановления связи прочитайте актуальное состояние, даже если новых команд не поступило. Не восстанавливайте отчет только из списка полученных уведомлений и не повторяйте бизнес-операцию ради проверки результата.
В браузерном клиенте модуля pull состояние соединения доступно через подписку BX.PullClient.SubscriptionType.Status. Обработчик получает объект {status}. Для этой подписки не нужны moduleId и command.
Значение status обозначает состояние клиента:
-
connect— клиент подключается, -
online— соединение установлено, -
offline— соединение отсутствует.
В функции mountReportView подписка на статус вызывает refresh() при значении online. Она обрабатывает и первое подключение, и восстановление связи. Функция destroy снимает эту подписку через unsubscribeStatus().
Выполняйте первоначальное чтение даже при подписке на статус. Клиент мог подключиться до регистрации обработчика. Функция mountReportView также проверяет состояние по таймеру и при возвращении к вкладке.
Браузерное событие online сообщает о доступности сети, но не подтверждает соединение с сервером очередей. Поэтому функция использует его лишь как повод повторить чтение.
После успешного контрольного чтения интерфейс покажет изменение, даже если команда не пришла. Если HTTP API недоступен, появится сообщение об ошибке, а функция повторит попытку. Фиксированное время восстановления не гарантируется.
В существующем коде можно использовать кастомное событие onPullStatus. Обработчик, зарегистрированный через BX.addCustomEvent('onPullStatus', handler), получает строку status. При закрытии интерфейса снимите его через BX.removeCustomEvent('onPullStatus', handler). Выберите один способ обработки статуса, чтобы не вызывать одну и ту же реакцию дважды.
Подписка типа Online относится к онлайн-статусам пользователей. Для состояния соединения используйте Status. Значение online не подтверждает получение конкретной команды или актуальность отчета. Чтобы проверить результат, запросите состояние отчета.
Браузерный и мобильный клиенты
Выберите зависимость по среде выполнения: браузерной странице, мобильной веб-странице или мобильному компоненту.
|
|| Контекст | Зависимость | Где подключить ||
|| Браузерная страница | pull.client | Зависимости JS-расширения или Extension::load ||
|| Веб-страница мобильного приложения | mobile.pull.client | Зависимости JS-расширения или Extension::load ||
|| JaNative component | pull/client/events | Файл deps.php ||
|| Offline WebComponent | pull/client/events | Раздел deps файла config.php ||
|#
Примеры используют элементы страницы и события браузера. При переносе в мобильный компонент адаптируйте вывод данных и снятие обработчиков при закрытии компонента. Проверьте доступность зависимостей в своем окружении.
Ограничение мобильного веб-клиента
При использовании mobile.pull.client проверьте результат подписки через экземпляр класса. Метод BX.PULL.subscribe может вернуть undefined вместо функции отписки. В этом случае после subscribe(new ReportCommands(...)) нельзя вызвать unsubscribe().
Чтобы сохранить возможность отписки, передавайте объект с moduleId и callback, как в разделе Обработать несколько команд. В такой форме метод возвращает функцию отписки. Это ограничение не относится к браузерному pull.client и не распространяется автоматически на другие мобильные клиенты.
Получение pull-команды работающим клиентом отличается от системного уведомления телефона. Условия отправки и отображения таких уведомлений приведены в статье Отправка мобильных push-уведомлений.
Совместимость с onPullEvent и проверка команд
Проверьте не только первое получение команды, но и повторное открытие интерфейса и пропущенные уведомления. Отдельно учитывайте существующий код с кастомными событиями.
Сопровождать обработчики onPullEvent
В существующем браузерном коде можно встретить BX.addCustomEvent('onPullEvent', handler). Общий обработчик получает module_id, command, params. Чтобы принимать команды одного модуля, используйте событие onPullEvent-<module_id>.
Пример. Именованная функция обрабатывает команду выбранного модуля и позволяет снять регистрацию.
function onReportCommand(command, params)
{
if (command === 'reportReady' && params && params.reportId === 42)
{
console.log('Отчет готов', params);
}
}
BX.addCustomEvent('onPullEvent-example.reports', onReportCommand);
// При завершении работы интерфейса
BX.removeCustomEvent('onPullEvent-example.reports', onReportCommand);
Вызывайте BX.removeCustomEvent при закрытии интерфейса, а не сразу после регистрации. Передайте ту же ссылку на функцию, которую использовали при подписке.
При переходе на BX.PULL.subscribe снимите прежний обработчик, иначе код обработки команды может выполниться дважды.
Проверить сценарии интерфейса
|
|| Проверка | Действие | Ожидаемый результат ||
|| Первая команда | Отправьте reportReady для отчета 42 из статьи Обновление страницы по команде сервера | Текст простого получателя изменился без перезагрузки ||
|| Другой объект | Передайте reportId: 43 | Обработчик отчета 42 не изменил интерфейс ||
|| Повтор | Отправьте ту же команду дважды | Итоговое состояние одинаковое. Отчет не формируется заново ||
|| Первоначальное чтение | Откройте отчет, завершенный до открытия страницы | Интерфейс с readReport показал сохраненное состояние без новой команды ||
|| Изменение во время чтения | Задержите ответ API и измените отчет с отправкой команды | После завершения запроса произошло повторное чтение ||
|| Старые данные | Верните версию 6 после уже показанной версии 7 | Интерфейс не заменил новое состояние старым ||
|| Пропущенная команда | Остановите получение pull-команд, сохраните отчет, затем восстановите связь | Контрольное чтение обнаружило изменение без повторного запуска операции ||
|| Ошибка чтения | Верните сетевую ошибку, отказ в доступе или удаление объекта | Интерфейс не выдал ошибку за готовность отчета и очистил прежний текст ||
|| Закрытие | Вызовите destroy во время запроса | Поздний ответ не меняет DOM, таймер и обработчики сняты ||
|| Повторное открытие | Закройте и откройте интерфейс, затем отправьте команду | Работает только новый экземпляр обработчика ||
|#
Перед переносом в рабочий проект проверьте доставку, восстановление соединения и снятие обработчиков на собственной установке.
Чтобы отправлять команды пользователям, которые наблюдают за объектом, настройте подписки по тегам.