WordPress как на ладони
Очень Удобный и Быстрый Хостинг для сайтов на WordPress. Пользуюсь сам и вам рекомендую!

Heartbeat API

Heartbeat API - это легкий способ периодически (каждые 15-120 секунд) опрашивать сервер на предмет новых данных и затем использовать их на стороне клиента (браузера).

Heartbeat API был добавлен в WordPress 3.6 и по началу был нужен для двух вещей:

  • Предупреждать пользователя о том, что пост редактируется кем-то другим в данный момент.
  • Проверять, не истекла ли сессия авторизации и запуск popup-окна с просьбой авторизоваться повторно.

Со временем этим механизмом стали пользоваться темы и плагины.

Heartbeat API по умолчанию работает только в админ-панели, но его легко можно использовать и во фронте.

Heartbeat API работает на основе классического AJAX в WordPress.

Принцип работы

  1. После загрузки страницы специальный JavaScript код (heartbeat код) запускает таймер. Через равные промежутки времени срабатывает JS событие heartbeat-send и запускаются подцепленные на это событие JS функции.

  2. JS функции формируют и отсылают данные в файл admin-ajax.php. В этом файле срабатывает WP хук:

    • wp_ajax_heartbeat - если авторизован
    • wp_ajax_nopriv_heartbeat - если не авторизован

    На оба эти хука по умолчанию подцеплены одноименные функции: wp_ajax_heartbeat() и wp_ajax_nopriv_heartbeat(). Таким образом, при хартбит запросе в PHP срабатывает одна из функций:

  3. Далее функция вызывает три хука, через которые по сути и нужно работать с Хартбит:

    Если авторизован:

    // если есть $_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 для фронтенда.

  4. После срабатывания всех вышеописанных хуков PHP возвращает обратно пользователю (в браузер) данные (переменная $response) в формате JSON.

  5. При получении данных в 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
Плагин размещен на github, потому вы можете не только его скачать, но и оставить свои пожелания или рассказать о баге.
Скачано: 199, размер: 44Кб

Состоит из двух файлов.

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' );

Литература

При создании статьи использовались материалы:

campusboy 4742youtube.com/c/wpplus
Создатель YouTube канала wp-plus, на котором делюсь своим опытом. Активный пользователь wp-kama.ru. WordPress-разработчик. Разработка сайтов и лендингов. Доработка существующих проектов. Сопровождение ресурсов.
Редакторы: Kama 9603
7 комментариев
    Войти