Heartbeat API
Heartbeat API - это легкий способ периодически (каждые 15-120 секунд) опрашивать сервер на предмет новых данных и затем использовать их на стороне клиента (браузера).
Heartbeat API был добавлен в WordPress 3.6 и по началу был нужен для двух вещей:
- Предупреждать пользователя о том, что пост редактируется кем-то другим в данный момент.
- Проверять, не истекла ли сессия авторизации и запуск popup-окна с просьбой авторизоваться повторно.
Со временем этим механизмом стали пользоваться темы и плагины.
Heartbeat API по умолчанию работает только в админ-панели, но его легко можно использовать и во фронте.
Heartbeat API работает на основе классического AJAX в WordPress.
Принцип работы
-
После загрузки страницы специальный JavaScript код (heartbeat код) запускает таймер. Через равные промежутки времени срабатывает JS событие heartbeat-send и запускаются подцепленные на это событие JS функции.
-
JS функции формируют и отсылают данные в файл admin-ajax.php. В этом файле срабатывает WP хук:
- wp_ajax_heartbeat - если авторизован
- wp_ajax_nopriv_heartbeat - если не авторизован
На оба эти хука по умолчанию подцеплены одноименные функции: wp_ajax_heartbeat() и wp_ajax_nopriv_heartbeat(). Таким образом, при хартбит запросе в PHP срабатывает одна из функций:
- wp_ajax_heartbeat() - если авторизован
- wp_ajax_nopriv_heartbeat() - если не авторизован
-
Далее функция вызывает три хука, через которые по сути и нужно работать с Хартбит:
Если авторизован:
// если есть $_POST['data'] $response = apply_filters( 'heartbeat_received', $response, $data, $screen_id ); // срабатывает всегда $response = apply_filters( 'heartbeat_send', $response, $screen_id ); // срабатывает всегда do_action( 'heartbeat_tick', $response, $screen_id );
Если не авторизован:
// если есть $_POST['data'] $response = apply_filters( 'heartbeat_nopriv_received', $response, $data, $screen_id ); // срабатывает всегда $response = apply_filters( 'heartbeat_nopriv_send', $response, $screen_id ); // срабатывает всегда do_action( 'heartbeat_nopriv_tick', $response, $screen_id );
$screen_id будет равен
front
для фронтенда. -
После срабатывания всех вышеописанных хуков PHP возвращает обратно пользователю (в браузер) данные (переменная $response) в формате JSON.
-
При получении данных в JS срабатывает событие heartbeat-tick.
На основе присланных данных производится та или иная работа: предложение авторизоваться снова, сообщения от клиента менеджеру, сообщения из чата Telegram, извещение о новых комментариях и так далее.
Использование Heartbeat API
Heartbeat API по умолчанию доступен только в админке. Чтобы использовать этот механизм во фронте (лицевой части), надо подключить скрипт heartbeat.js или же указать его в зависимостях в своём скрипте.
// 1 Вариант. Подключаем JS-скрипт heartbeat wp_enqueue_script( 'heartbeat' ); // 2 Вариант. Подключаем heartbeat, как зависимость в нашем скрипте wp_enqueue_script( 'script-name', get_template_directory_uri() .'/js/example.js', array('heartbeat'), '1.0', true );
Далее для использования Heartbeat API, нужно пройти три этапа. Не важно, делается это для фронта или админки.
1. Отправка данных на сервер
JS cобытие heartbeat-send
срабатывает перед отправкой данных на сервер и это самый удачный момент добавить свои данные в коллекцию данных Heartbeat.
В дефолтной сборке WordPress во вкладке браузера можно увидеть при ajax запросе нечто подобное:
interval: 60 _nonce: bab7ce80c5 action: heartbeat screen_id: dashboard has_focus: false
Добавим в эту коллекцию свои данные:
По правилам хорошего тона JS код надо размещать в файле и подключать на хуке admin_enqueue_scripts (если речь идет об админке), но для упрощения примера выведем его на хуке admin_print_footer_scripts
<?php // Подключим наш скрипт в админке add_action( 'admin_print_footer_scripts', function () { ?> <script> jQuery(document).on( 'heartbeat-send', function (event, data) { // Добавляем свои данные в коллекцию данных Heartbeat. data.myplugin_field = 'some_data'; }); </script> <?php } );
Во вкладке браузера можно посмотреть, как данные отправились:
data[myplugin_field]: some_data interval: 60 _nonce: bab7ce80c5 action: heartbeat screen_id: dashboard has_focus: false
В примере была отправлена просто строка some_data, но отправлять можно любые данные: числа, массивы, объекты и т.д.
2. Прием данных на сервере и ответ
Прежде всего давайте посмотрим, что по умолчанию возвращает сервер:
{ "wp-auth-check":true, "server_time":1520878390 }
На хуке heartbeat_received можно дополнить массив, которые формируют ядро/плагины/тема, своими данными:
// Фильтр для работы с присланными Heartbeat данными. add_filter( 'heartbeat_received', 'myplugin_receive_heartbeat', 10, 2 ); /** * Принимает Heartbeat данные и формирует ответ. * * @param array $response Данные Heartbeat для отправки во фронтэнд. * @param array $data Данные, присланные с фронтэнда (unslashed). * * @return array */ function myplugin_receive_heartbeat( $response, $data ) { // Если наши данные не пришли, то возвращаем оригинал ответа. if ( empty( $data['myplugin_field'] ) ) { return $response; } /** * Обращаемся к нашим данным. * Ключ массива совпадает со свойством объекта в JS, в который мы поместили данные. */ $received_data = $data['myplugin_field']; // Для примера посчитаем сколько в переданной строке символов. $count_symbol = mb_strlen( $received_data ); $response['myplugin_strlen'] = 'Количество знаков в переданной фразе ' . $count_symbol; // Вернем дополненный массив Heartbeat нашими данными. return $response; }
Теперь сервер вернет следующий набор данных:
{ "myplugin_strlen":"Количество знаков в переданной фразе 9" "wp-auth-check":true, "server_time":1520878390 }
3. Обработка ответа сервера
После того, как сервер вернул объект с данными, обработаем их:
<?php add_action( 'admin_print_footer_scripts', function () { ?> <script> jQuery( document ).on( 'heartbeat-tick', function ( event, data, textStatus, jqXHR ) { // event - объект события // data - приходящие данные // textStatus - статус выполнения запроса, к примеру success. // jqXHR - объект запроса console.log(event, data, textStatus, jqXHR); // Проверим, есть ли наши данные и если нет - остановим скрипт. if ( ! data.myplugin_strlen ) { return; } // Выведем данные на экран через alert() alert( data.myplugin_strlen ); }); </script> <?php } );
Мы рассмотрели три этапа, но не каждая задача нуждается в первом или третьем этапе.
Под капотом
PHP
PHP функционал сосредоточен в файле ajax-actions.php.
Функция wp_ajax_heartbeat()
Эту функцию не нужно использовать, она автоматом вызывается в файле admin-ajax.php через ajax хуки. Она и проверяет запрос и вызывает хуки связанные с хартбит: heartbeat_send, heartbeat_tick.
Другими словами функция обрабатывает Heartbeat запросы от зарегистрированных пользователей. Прежде чем обработать запрос, проверяет наличие nonce кода. Если его нет - прерывает работу и возвращает ошибку. Если срок действия nonce кода истёк - возвращает информацию об этом и на странице появляется всплывающее окно с просьбой авторизоваться. Также в ответ добавляет время сервера функцией time().
Свой функционал можно добавить через фильтры и события. См. ниже.
Фильтр heartbeat_received
Фильтрует полученные данные heartbeat. Срабатывает, если есть данные в $data
.
- $response(массив)
- Данные для ответа Heartbeat.
- $data(массив)
- Присланные данные в массиве $_POST.
- $screen_id(строка)
- ID экрана. В php совпадает с
$current_screen->id
и c глобальнымpagenow
в JS.
Пример
add_filter( 'heartbeat_received', 'myplugin_receive_heartbeat', 10, 3 ); function myplugin_receive_heartbeat( $response, $data, $screen_id ) { // Если наши данные не пришли, то возвращаем оригинал ответа. if ( empty( $data['myplugin_field'] ) ) { return $response; } /** * Обращаемся к нашим данным. * Ключ массива совпадает с меткой (handle) в JS, которая была присвоена данным. */ $received_data = $data['myplugin_field']; // Пусть приходит строка и для примера посчитаем сколько в этой строке символов. $count_symbol = mb_strlen( $received_data ); $response['myplugin_field_strlen'] = 'Количество знаков в переданной фразе ' . $count_symbol; // Вернем дополненный массив Heartbeat нашими данными. return $response; }
Фильтр heartbeat_send
Фильтрует данные ответа Heartbeat.
- $response(массив)
- Данные для ответа Heartbeat.
- $screen_id(строка)
- ID экрана.
Пример из ядра WordPress
В ответ heartbeat добавляется информация о статусе авторизации пользователя.
add_filter( 'heartbeat_send', 'wp_auth_check' ); function wp_auth_check( $response ) { $response['wp-auth-check'] = is_user_logged_in() && empty( $GLOBALS['login_grace_period'] ); return $response; }
Простой пример:
Добавим в ответ heartbeat информацию о количестве непроверенных комментариев.
add_filter( 'heartbeat_send', 'myplugin_send_heartbeat', 10, 2 ); function myplugin_send_heartbeat( $response, $screen_id ) { $comments_count = wp_count_comments(); $response['moderated'] = $comments_count->moderated; return $response; }
Событие heartbeat_tick
Тоже что и heartbeat_send, только это событие, и в нем не надо ничего изменять, а принято делать что-либо перед ответом сервера браузеру. Т.е. это отличное место для реализации любых действий действий, например отправка письма.
Срабатывает после формирования heartbeat данных, но перед тем, как сервер отдаст их клиенту.
- $response(массив)
- Данные для ответа Heartbeat.
- $screen_id(строка)
- ID экрана.
Пример
Если пришли какие-то определенные данные, известить администратора письмом.
add_action( 'heartbeat_tick', 'myplugin_heartbeat_tick', 10, 2 ); function myplugin_heartbeat_tick( $response, $screen_id ) { if( ! empty($response['some_key']) ){ wp_mail('admin@test.com', 'Тема письма', 'Содержание письма'); } }
Фильтр heartbeat_settings
Позволяет изменить опции в JS объекте wp.heartbeat.settings
.
// Изменит дефолтный интервал опроса сервера с 60 секунд на 30. add_filter( 'heartbeat_settings', function ( $settings ) { $settings['mainInterval'] = 30; return $settings; } );
Хуки и функция wp_ajax_nopriv_heartbeat()
Для взаимодействия с неавторизованными пользователями во фронте, вместо функции wp_ajax_heartbeat() срабатывает функция wp_ajax_nopriv_heartbeat(). Она отличает от первой лишь тем, что не проверяет nonce код и статус авторизации, а также в её состав входят хуки с другими именами, но аналогичным поведением:
JavaScript и jQuery
JavaScript функционал сосредоточен в файле heartbeat.js.
Событие heartbeat-send
На этом событии можно добавить свои данные в коллекцию heartbeat перед отправкой их на сервер.
jQuery( document ).on( 'heartbeat-send', function ( event, data ) { // Добавляем данные в Heartbeat. Затем на сервере данные можно взять из массива по ключу myplugin_customfield. data.myplugin_customfield = 'some_data'; });
Событие heartbeat-tick
Срабатывает каждый раз, когда от сервера приходят heartbeat данные. На этом событии оперируем с присланными данными.
jQuery(document).on('heartbeat-tick', function (event, data, textStatus, jqXHR) { // event - объект события // data - приходящие данные // textStatus - статус выполнения запроса, к примеру success. // jqXHR - объект запроса console.log(event, data, textStatus, jqXHR); // Проверяем, пришли ли данные от нашего плагина/темы. if (!data.data.myplugin_responce_data) { return; } alert('Эти данные вернулись от моего плагина/темы: ' + data.myplugin_responce_data); });
Также можно отследить событие для нужного ответа по метке данных:
$(document).on( 'heartbeat-tick.myplugin_responce_data', function( event, data, textStatus, jqXHR ) { // Код });
Событие heartbeat-error
Срабатывает каждый раз, когда запрос к серверу был провален, то есть сработало $.ajax().fail(). На этом событии можно отследить, какая именно произошла ошибка и сделать что-либо по необходимости.
jQuery( document ).on( 'heartbeat-error', function ( jqXHR, textStatus, error ) { // jqXHR - объект запроса. // textStatus - статус выполнения запроса. // error - текстовый вариант ошибки (может быть abort, timeout, error, parsererror, empty, unknown). console.log(jqXHR, textStatus, error); if( 'timeout' === error ){ alert('Прошло 30 секунд, а сервер так и не ответил. И это печально!'); } });
Остальные события
Heartbeat API имеет в составе ещё несколько событий, но они используются при разработке крайне редко:
- heartbeat-connection-lost
- heartbeat-connection-restored
- heartbeat-nonces-expired
JavaScript объект wp.heartbeat
Работа с событиями напрямую не единственный способ взаимодействовать с Heartbeat API. Дело в том, что сердцем механизма является класс Heartbeat()
, экземпляр которого помещен в переменную window.wp.heartbeat
и доступен для разработчиков при написании JavaScript кода.
Рассмотрим методы этого класса.
Метод enqueue()
Добавляет данные в очередь для отправки в следующем XHR. Поскольку данные отправляются асинхронно, то эта функция не возвращает ответ XHR. Увидеть ответ можно на событии heartbeat-tick
. Если одна и та же метка используется несколько раз, данные не перезаписываются, когда третий аргумент noOverwrite
имеет значение true
. Используйте wp.heartbeat.isQueued('handle'), чтобы увидеть, были ли какие-либо данные уже поставлены в очередь для этой метки.
wp.heartbeat.enqueue( handle, data, noOverwrite );
- handle(строка)
- Уникальная метка (дескриптор) отправляемых данных. На бэкэнде используется как ключ массива для получения переданных значений.
- data(любые)
- Отправляемые данные.
- noOverwrite(логический)
- Перезаписывать ли существующие данные в очереди.
Возвращает true, если данные были поставлены в очередь и false, если нет.
// Добавим в очередь данные для отправки. wp.heartbeat.enqueue( 'my-plugin-data', { 'param1': 'param value 1', 'param2': 'param value 2', }, false );
Метод getQueuedItem()
Возвращает данные, стоящие в очереди, по их метке (Handle).
wp.heartbeat.getQueuedItem( handle );
- handle(строка)
- Уникальная метка (дескриптор) отправляемых данных.
// Добавляем данные в очередь wp.heartbeat.enqueue( 'my-plugin', { 'param1': 'value 1', 'param2': 'value 2', }, false ); // Получим данные var myData = wp.heartbeat.getQueuedItem('my-plugin') console.log(myData); /* вернётся { 'param1': 'value 1', 'param2': 'value 2', } /
Метод isQueued()
Проверяет, находятся ли данные с определенной меткой в очереди.
wp.heartbeat.isQueued( handle );
- handle(строка)
- Уникальная метка (дескриптор) отправляемых данных.
Возвращает true, если данные есть в очереди и false, если нет.
// В консоли отобразится false console.log( wp.heartbeat.isQueued('my-plugin') ); // Добавляем данные в очередь wp.heartbeat.enqueue( 'my-plugin', { 'param1': 'value 1', 'param2': 'value 2', }, false ); // В консоле отобразится true console.log( wp.heartbeat.isQueued('my-plugin') ); // true
Метод dequeue()
Удаляет из очереди данные по их метке (handle).
wp.heartbeat.dequeue( handle );
- handle (строка)
- Уникальная метка (дескриптор) отправляемых данных.
// Добавляем данные в очередь wp.heartbeat.enqueue( 'my-plugin', { 'param1': 'value 1', 'param2': 'value 2', }, false ); // В консоле отобразится true, данные 'my-plugin' в очереди имеются console.log( wp.heartbeat.isQueued('my-plugin') ); // true // Удалим данные из очереди wp.heartbeat.dequeue('my-plugin'); // В консоле отобразится false, данные 'my-plugin' в очереди отсутствуют console.log( wp.heartbeat.isQueued('my-plugin') ); // false
Метод interval()
Устанавливает или возвращает интервал опроса сервера в секундах.
wp.heartbeat.interval( speed, ticks );
- speed(строка/число)
- Скорость опроса в секундах. Может быть 'fast' или 5, 15, 30, 60, 120, 'long-polling' (экспериментально). Если окно не в фокусе, интервал замедляется до 2 минут. Если количество секунд передаётся как число, то тип должен быть именно числом, а не строкой.
По умолчанию: 60 - ticks(число)
- Данный аргумент применяется, если аргумент
speed
равен 'fast' или 5. Позволяет указать сколько раз посылать запросы с этим интервалом. Можно указать не более 30, то есть максимум каждые 5 секунд сервер будет опрашиваться в течение 2 минут 30 секунд. После этого speed возвращается к значению по умолчанию, то есть 60 секунд.
По умолчанию: 30
// В консоли отобразится 60 (значение по умолчанию) console.log( wp.heartbeat.interval() ); // 60 wp.heartbeat.interval(30); console.log( wp.heartbeat.interval() ); //> 30
Пример передачи переменной в качестве числа:
var interval = mytheme.custom_interval; // переменная содержит 30 wp.heartbeat.interval( parseInt( interval ) ); console.log( wp.heartbeat.interval() ); // 30
Метод hasFocus()
Проверяет имеет ли окно (или любой локальный iframe в нем) фокус или активен ли пользователь. Возвращает true или false. Обращается к свойству settings.hasFocus. Работает на основе нативного document.hasFocus() (если доступен и обновляет settings.hasFocus по таймеру каждые 10 секунд). Если нужно проверять фокус чаще, чем каждые 10 секунд, используйте document.hasFocus().
wp.heartbeat.hasFocus();
Ниже код каждые 5 секунд будет проверять, активна ли вкладка в браузере, где используется heartbeat, и выводить в консоль браузера соответствующее сообщение.
setInterval(function () { if (wp.heartbeat.hasFocus()) { console.log('Приложение в фокусе.'); } else { console.log('Фокус потерян. Вы свернули браузер, переключись на другую вкладку или приложение.'); } }, 5000);
Метод disableSuspend()
Отключает приостановку. Следует использовать только в том случае, когда heartbeat выполняет такие важные задачи, как автосохранение, пост-блокировка и т.д. Использование этого функционала на многих экранах может привести к перегрузке учетной записи хостинга пользователя, если несколько окон/вкладок браузера остаются открытыми в течение длительного времени.
wp.heartbeat.disableSuspend();
Метод connectNow()
Сразу отправляет heartbeat запрос вне зависимости от состояния hasFocus
. Не будет открывать два одновременных соединения. Если подключение выполняется, будет подключен снова сразу после завершения текущего подключения. Особенно удобен при тестировании heartbeat функционала, так как можно сделать запрос сразу, в том числе и из консоли браузера.
wp.heartbeat.connectNow();
Метод hasConnectionError()
Проверяет, существует ли ошибка подключения.
if (wp.heartbeat.hasConnectionError()) { console.log('Последний запрос был завершен с ошибкой.'); } else { console.log('Ошибок нет.'); }
Примеры
Нет ничего лучше, чем примеры в виде реальных плагинов, которые всегда можно найти в репозитории WordPress.
Слежение за комментариями в админке
Данный плагин, как пример, при heartbeat запросе собирает данные о комментариях, ожидающих модерации, и обновляет счётчики в сайдбаре и админбаре. Срабатывает, как в админке, так и во фронте. И только у тех пользователей, у которых есть права модерировать комментарии.

Состоит из двух файлов.
admin-comment-notice.php
add_action( 'wp_enqueue_scripts', 'acn_enqueue_scripts' ); add_action( 'admin_enqueue_scripts', 'acn_enqueue_scripts' ); add_filter( 'heartbeat_send', 'acn_heartbeat_send' ); /** * Добавляет данные в heartbeat ответ. * * @param array $response * * @return array */ function acn_heartbeat_send( $response ) { if ( ! current_user_can( 'moderate_comments' ) ) { return $response; } $count = wp_count_comments(); $count = absint( $count->moderated ); $i18n = number_format_i18n( $count ); // Админ-сайдбар $menu = '<span class="awaiting-mod count-' . $count . '"><span class="pending-count">' . $i18n . '</span></span>'; $menu = sprintf( __( 'Comments %s' ), $menu ); // Админ-бар $text = sprintf( _n( '%s comment awaiting moderation', '%s comments awaiting moderation', $count ), $i18n ); $bar = '<span class="ab-icon"></span>'; $bar .= '<span class="ab-label awaiting-mod pending-count count-' . $count . '" aria-hidden="true">' . $i18n . '</span>'; $bar .= '<span class="screen-reader-text">' . $text . '</span>'; // Данные $response['acn'] = array( 'menu' => $menu, 'bar' => $bar, 'count' => $i18n, ); return $response; } /** * Подключает скрипт плагина. */ function acn_enqueue_scripts() { if ( is_admin_bar_showing() && current_user_can( 'moderate_comments' ) ) { $script_url = plugins_url( 'scripts.js', __FILE__ ); wp_enqueue_script( 'acn-script', $script_url, array( 'heartbeat' ) ); } }
scripts.js
jQuery(document).on('heartbeat-tick', function (event, data) { // Проверим, есть ли наши данные и если нет - остановим скрипт. if (data.acn === undefined) { return; } // Находим контейнеры, в которых будем менять содержимое. var $menu = jQuery('#menu-comments').find('.wp-menu-name'); var $bar = jQuery('#wp-admin-bar-comments').find('a'); // Изменяем содержимое контейнеров. jQuery($menu).html(data.acn.menu); jQuery($bar).html(data.acn.bar); });
В WordPress по умолчанию
После отправки Хартбит запроса, WP обрабатывает по умолчанию всего несколько операций. Чтобы узнать какие, посмотрим, что прикрепляется к хукам: heartbeat_received, heartbeat_send и heartbeat_tick. Т.е. как ВП меняет ответ сервера.
heartbeat_received
Файл wp-admin/includes/admin-filters.php:
add_filter( 'heartbeat_received', 'wp_check_locked_posts', 10, 3 ); add_filter( 'heartbeat_received', 'wp_refresh_post_lock', 10, 3 ); add_filter( 'heartbeat_received', 'heartbeat_autosave', 500, 2 );
Файл wp-includes/class-wp-customize-manager.php:
add_filter( 'heartbeat_received', array( $this, 'check_changeset_lock_with_heartbeat' ), 10, 3 );
heartbeat_send
Файл: wp-includes/default-filters.php
add_filter( 'heartbeat_send', 'wp_auth_check' ); add_filter( 'heartbeat_nopriv_send', 'wp_auth_check' );
heartbeat_tick
Ничего не прикрепляется по умолчанию.
Базовый настройки Хартбит, устанавливаются через фильтр:
add_filter( 'heartbeat_settings', 'wp_heartbeat_settings' );
Литература
При создании статьи использовались материалы:
- Исходный код файла heartbeat.js.
- Официальная документация по Heartbeat API
- Статья на code.tutsplus.com - The Heartbeat API: Getting Started
- Статья на code.tutsplus.com - Heartbeat API: Using Heartbeat in a Plugin