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 подія heartbeat-sendспрацьовує перед відправкою даних на сервер і це найвдаліший момент додати свої дані до колекції даних Heartbeat.

У дефолтній збірці WordPress у вкладці браузера можна побачити при запитанні ajax щось подібне:

interval: 60
_nonce: bab7ce80c5
action: heartbeat
screen_id: dashboard
has_focus: false

Додамо до цієї колекції свої дані:

За правилами гарного тону JS код треба розміщувати у файлі та підключати на хуку 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('[email protected]', 'Тема листа', 'Зміст листа');
	}
}


Фільтр 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() . Вона відрізняє від першої лише тим, що не перевіряє код і статус авторизації, а також до її складу входять хуки з іншими іменами, але аналогічною поведінкою:


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, тому ви можете не тільки його скачати, але й залишити свої побажання або розповісти про баг.
Завантажено: 134, розмір: 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');


Література

Під час створення статті використовувалися матеріали:

Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *