#javascript #reactjs #axios #sendbeacon
#javascript #браузер #окно #dom-события
Вопрос:
Я пытаюсь выяснить, когда пользователь покинул указанную страницу. Нет проблем выяснить, когда он использовал ссылку внутри страницы для перехода, но мне нужно отметить что-то вроде того, когда он закрыл окно или набрал другой URL-адрес и нажал enter. Второе не так важно, но первое важно. Итак, вот вопрос:
Как я могу увидеть, когда пользователь закрыл мою страницу (событие захвата window.close), а затем… на самом деле это не имеет значения (мне нужно отправить AJAX-запрос, но если я могу заставить его запустить оповещение, я могу сделать все остальное).
Ответ №1:
Обновлено 2021
TL; DR
Beacon API — это решение этой проблемы (почти в каждом браузере).
Предполагается, что запрос beacon завершится, даже если пользователь покинет страницу.
Когда следует запускать запрос Beacon?
Это будет зависеть от вашего варианта использования. Если вы хотите перехватить любой пользовательский выход, visibilitychange
(not unload
) — это последнее событие, которое разработчики надежно наблюдают в современных браузерах.
ПРИМЕЧАНИЕ: пока реализация visibilitychange
не согласована между браузерами, вы можете обнаружить это с помощью lifecycle.js библиотека.
# lifecycle.js (1K) for cross-browser compatibility
# https://github.com/GoogleChromeLabs/page-lifecycle
<script defer src="/path/to/lifecycle.js"></script>
<script defer>
lifecycle.addEventListener('statechange', function(event) {
if (event.originalEvent == 'visibilitychange' amp;amp; event.newState == 'hidden') {
var url = "https://example.com/foo";
var data = "bar";
navigator.sendBeacon(url, data);
}
});
</script>
Подробные сведения
Предполагается, что запросы Beacon выполняются до завершения, даже если пользователь покидает страницу — переключается на другое приложение и т. Д. — Без блокирования рабочего процесса пользователя.
Под капотом он отправляет запрос POST вместе с учетными данными пользователя (cookie) с учетом ограничений CORS.
var url = "https://example.com/foo";
var data = "bar";
navigator.sendBeacon(url, data);
Вопрос в том, когда отправлять запрос на маяк. Особенно, если вы хотите подождать до последнего момента, чтобы отправить информацию о сеансе, состоянии приложения, аналитику и т. Д.
Раньше было обычной практикой отправлять его во время unload
события, но изменения в управлении жизненным циклом страницы, управляемые мобильным UX, убили этот подход. Сегодня большинство мобильных рабочих процессов (переключение на новую вкладку, переключение на рабочий стол, переключение на другое приложение …) не запускают unload
событие.
Если вы хотите что-то делать, когда пользователь выходит из вашего приложения / страницы, теперь рекомендуется использовать visibilitychange
событие и проверять переход из passive
состояния в hidden
состояние.
document.addEventListener('visibilitychange', function() {
if (document.visibilityState == 'hidden') {
// send beacon request
}
});
Переход в режим hidden часто является последним изменением состояния, которое надежно наблюдается разработчиками (это особенно верно для мобильных устройств, поскольку пользователи могут закрывать вкладки или само приложение браузера, а события beforeunload , pagehide и unload в этих случаях не запускаются).
Это означает, что вы должны рассматривать скрытое состояние как вероятный конец сеанса пользователя. Другими словами, сохраняйте любое несохраненное состояние приложения и отправляйте любые неотправленные аналитические данные.
Подробности Page lifecyle API
описаны в этой статье.
Однако реализация visibilitychange
события, а также Page lifecycle API
не согласована между браузерами.
Пока реализация браузера не догонит, используя lifecycle.js рекомендации по работе с библиотеками и жизненным циклом страниц кажутся хорошим решением.
# lifecycle.js (1K) for cross-browser compatibility
# https://github.com/GoogleChromeLabs/page-lifecycle
<script defer src="/path/to/lifecycle.js"></script>
<script defer>
lifecycle.addEventListener('statechange', function(event) {
if (event.originalEvent == 'visibilitychange' amp;amp; event.newState == 'hidden') {
var url = "https://example.com/foo";
var data = "bar";
navigator.sendBeacon(url, data);
}
});
</script>
Для получения дополнительных данных о надежности событий жизненного цикла ванильной страницы (без lifecycle.js ), есть и такое исследование.
Блокировщики рекламы
У блокировщиков рекламы, похоже, есть опции, которые блокируют запросы sendBeacon.
Межсайтовые запросы
Запросы Beacon — это запросы POST, которые включают файлы cookie и подпадают под спецификацию CORS. Подробная информация.
Комментарии:
1. Этот ответ изменил мою жизнь! Большое спасибо!
2. MDN говорит
unload and beforeunload aren’t the right events to use with sendBeacon. Instead, use visibilitychange
, что это бесполезно developer.mozilla.org/en-US/docs/Web/API/Navigator/sendBeacon3. @godblessstrawberry Действительно. MDN недавно отменила свои рекомендации по использованию
unload
: github.com/mdn/sprints/issues/3722 . Я переписал ответ, чтобы учесть это.4. @AlfieRobles пожалуйста, проверьте изменения в этом ответе, если вы полагаетесь на
unload
событие для получения важных данных.5. Это выглядит многообещающе, но, похоже, мы не можем установить заголовки запросов с помощью запросов-маяков? Страница MDN для
sendBeacon
них не упоминается, но я нашел эту статью , в которой отмечается, чтоContent-Type
это возможно. Я ошибаюсь в этом, или это, вероятно, потребует обновления принимающего API, чтобы разрешить обход таких вещей, как аутентификация (или обработка их с помощью полезной нагрузки запроса, а не заголовков)?
Ответ №2:
Существуют unload
beforeunload
события и javascript, но они ненадежны для запроса Ajax (не гарантируется, что запрос, инициированный в одном из этих событий, дойдет до сервера).
Поэтому делать это крайне не рекомендуется, и вам следует поискать альтернативу.
Если вам это определенно нужно, рассмотрите решение в стиле «ping». Отправляйте запрос каждую минуту, в основном сообщая серверу: «Я все еще здесь». Затем, если сервер не получает такой запрос более двух минут (вы должны учитывать задержки и т. Д.), Вы рассматриваете клиент в автономном режиме.
Другим решением было бы использовать unload
или beforeunload
выполнить запрос Sjax (синхронный JavaScript и XML), но это совершенно не рекомендуется. Выполнение этого в основном приведет к зависанию браузера пользователя до завершения запроса, что им не понравится (даже если запрос займет немного времени).
Комментарии:
1. Ну, если он выполняет выход из системы, то я видел рабочие примеры, когда при выгрузке открывается всплывающее окно, которое затем выводит пользователя из системы (решает ли это проблему без гарантии?).
2. Нет. Бьюсь об заклад, что существуют блокировщики всплывающих окон, которые не разрешают всплывающие окна при этих событиях. Кроме того, ужасно открывать всплывающее окно, когда пользователь закрывает вкладку / окно.
3. Я предполагаю, что проблема в том, что запрос требует времени, и браузер может быть завершен с выгрузкой к моменту его завершения (так что он может никогда не завершиться)?
4. Я на самом деле делаю то, что я все еще здесь. Но это немного медленно и снижает удобство работы пользователя. Также уменьшение интервала «я все еще здесь» может привести к остановке сервера при высокой скорости пользователей. Итак, я пытался заменить его чем-то другим. Когда вы говорите, что запрос ненадежен, насколько ненадежен вы имеете в виду? Я могу использовать это и удвоить его при отправке сообщений с более высоким интервалом, вероятность успеха составляет более 50%. Я думаю, что это может повысить производительность с обеих сторон (сервера и клиента).
5. Я думаю, это было бы вашим лучшим выбором (хотя вам все равно придется выполнять пинг, если вы хотите быть уверены на 100%). Кстати, способ сделать это —
return "a string";
в вашемbeforeunload
обработчике (выполнение aconfirm()
не сработает).
Ответ №3:
1) Если вы ищете способ работать во всех браузерах, то самый безопасный способ — отправить синхронный AJAX на сервер. Это не очень хороший метод, но, по крайней мере, убедитесь, что вы не отправляете слишком много данных на сервер, а сервер работает быстро.
2) Вы также можете использовать асинхронный AJAX-запрос и использовать функцию ignore_user_abort на сервере (если вы используете PHP). Однако ignore_user_abort во многом зависит от конфигурации сервера. Убедитесь, что вы хорошо его протестировали.
3) Для современных браузеров вы не должны отправлять AJAX-запрос. Вы должны использовать новый метод navigator.sendBeacon для отправки данных на сервер асинхронно и без блокировки загрузки следующей страницы. Поскольку вы хотите отправить данные на сервер до того, как пользователь покинет страницу, вы можете использовать этот метод в обработчике событий выгрузки.
$(window).on('unload', function() {
var fd = new FormData();
fd.append('ajax_data', 22);
navigator.sendBeacon('ajax.php', fd);
});
Также, похоже, существует полизаполнение для sendBeacon. Он прибегает к отправке синхронного AJAX, если метод изначально недоступен.
ВАЖНО ДЛЯ МОБИЛЬНЫХ УСТРОЙСТВ: обратите внимание, что для мобильных устройств не гарантируется запуск обработчика события выгрузки. Но событие visibilitychange гарантированно будет запущено. Поэтому для мобильных устройств ваш код сбора данных может потребовать небольшой настройки.
Вы можете обратиться к моей статье в блоге для кодовой реализации всех 3 способов.
Комментарии:
1. В 2018 году это лучший ответ. Используйте beacon API. Прочитайте статью в блоге, упомянутую в ответе ( usefulangle.com/post/62 /… ) для получения более подробной информации.
2. Я знал, что он существует, но не мог вспомнить его название. Это правильный ответ IMO «navigator.sendBeacon» <—
Ответ №4:
Я также хотел добиться той же функциональности и наткнулся на этот ответ от Felix (не гарантируется, что запрос, инициированный в одном из этих событий, дойдет до сервера).
Чтобы сделать запрос доступным для сервера, мы попробовали следующий код:-
onbeforeunload = function() {
//Your code goes here.
return "";
}
Мы используем браузер IE, и теперь, когда пользователь закрывает браузер, он получает диалоговое окно подтверждения из-за return "";
amp; ожидает подтверждения пользователя, и это время ожидания заставляет запрос доходить до сервера.
Комментарии:
1. кажется, что текущий Chrome игнорирует это вообще, если в функции beforeunload есть нечто большее, чем просто строковый оператор retrun. Таким образом, вы можете вернуть «Вы хотите закрыть?», Но вы не можете сделать ничего другого…
Ответ №5:
Спустя годы после публикации вопроса я улучшил реализацию, включая nodejs и socket.io (https://socket.io ) (вы можете использовать любой тип сокета, если уж на то пошло, но это был мой личный выбор).
В основном я открываю соединение с клиентом, и когда он зависает, я просто сохраняю данные / делаю все, что мне нужно. Очевидно, что это не может использоваться для отображения чего-либо / перенаправления клиента (поскольку вы делаете это на стороне сервера), но это то, что мне действительно было нужно тогда.
io.on('connection', function(socket){
socket.on('disconnect', function(){
// Do stuff here
});
});
Итак … в настоящее время я думаю, что это было бы лучше (хотя и сложнее реализовать, потому что вам нужен узел, сокет и т. Д., Но это не так сложно; должно занять около 30 минут или около того, если вы делаете это в первый раз), Чем версия выгрузки.
Ответ №6:
Выбранный ответ верен: вы не можете гарантировать, что браузер отправит запрос xhr, но в зависимости от браузера вы можете надежно отправить запрос при закрытии вкладки или окна.
Обычно браузер закрывается до фактического выполнения xhr.send() . Chrome и edge выглядят так, как будто они ожидают, пока цикл событий javascript опустеет, прежде чем закрыть окно. Они также запускают запрос xhr в другом потоке, отличном от цикла событий javascript. Это означает, что если вы сможете достаточно долго сохранять цикл событий полным, xhr будет успешно запущен. Например, я тестировал отправку запроса xhr, а затем считал до 100 000 000. Для меня это работало очень последовательно как в Chrome, так и в edge. Если вы используете angularjs, перенос вашего вызова в $http в $apply выполняет то же самое.
IE, похоже, немного отличается. Я не думаю, что IE ожидает, пока цикл событий опустеет, или даже текущий фрейм стека опустеет. Хотя иногда он будет корректно отправлять запрос, гораздо чаще (в 80-90% случаев) происходит то, что IE закрывает окно или вкладку до полного выполнения запроса xhr, что приводит к отправке только частичного сообщения. В основном сервер получает post-запрос, но тела нет.
Для потомков, вот код, который я использовал, прикрепленный в качестве функции прослушивателя window.onbeforeunload:
var xhr = new XMLHttpRequest();
xhr.open("POST", <your url here>);
xhr.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
var payload = {id: "123456789"};
xhr.send(JSON.stringify(payload));
for(var i = 0; i < 100000000; i ) {}
Я тестировал в:
Chrome 61.0.3163.100
IE 11.608.15063.0CO
Edge 40.15063.0.0
Комментарии:
1. Время, затрачиваемое на цикл for, зависит от процессора, движка JS и других факторов. Чуть менее плохой идеей было бы заблокировать цикл событий на 100 мс или любое другое значение, которое считается достаточно длительным после экспериментов
Ответ №7:
Попробуйте это. Я решил эту проблему в javascript, отправив вызов ajax на сервер при просмотре или закрытии вкладки. У меня возникла проблема с обновлением страницы, потому что функция onbeforeunload включает обновление страницы. производительность.навигация.тип == 1 должен изолировать обновление от закрытия (в браузере mozzila).
$(window).bind('mouseover', (function () { // detecting DOM elements
window.onbeforeunload = null;
}));
$(window).bind('mouseout', (function () { //Detecting event out of DOM
window.onbeforeunload = ConfirmLeave;
}));
function ConfirmLeave() {
if (performance.navigation.type == 1) { //detecting refresh page(doesnt work on every browser)
}
else {
logOutUser();
}
}
$(document).bind('keydown', function (e) { //detecting alt F4 closing
if (e.altKey amp;amp; e.keyCode == 115) {
logOutUser();
}
});
function logOutUser() {
$.ajax({
type: "POST",
url: GWA("LogIn/ForcedClosing"), //example controller/method
async: false
});
}
Комментарии:
1. Привет и добро пожаловать в SO. Не отклонение (поскольку вы новичок :)) но ваш ответ не привносит ничего нового в принятый ответ. Также, если я правильно его читаю, он запускает функцию ConfirmLeave каждый раз, когда пользователь перемещает мышь за пределы окна, что приводит к ложным запросам (например, каждый раз, когда он перемещает курсор мыши на панель задач или что-то в этом роде). Также он не обрабатывает какие-либо случаи касания / перемещения (поскольку событие привязывается только при наведении курсора мыши, удаляется при повторном запуске и никогда не привязывается обратно). Также рассмотрим случай простого нажатия клавиши f5 (в вашем примере это тоже не работает).
2. В 2011 году (когда я опубликовал вопрос) это было невозможно (или, по крайней мере, не так просто, как сегодня), но в настоящее время вы можете использовать для этого websocket, что в основном дает вам преимущества решения в стиле ping без их недостатков. недостатки. Если вы хотите что-то проверить по этому поводу, я бы с удовольствием поддержал подобный ответ (я подумывал о том, чтобы опубликовать его сам, но… обленился 🙂 ).
Ответ №8:
Я согласен с идеей Феликса, и я решил свою проблему с помощью этого решения, и теперь я хочу очистить решение на стороне сервера:
1. отправьте запрос со стороны клиента на сервер
2. сохраните время последнего запроса, полученного в переменной
3. проверьте время сервера и сравните его с переменной последнего полученного запроса
4. если результат превышает ожидаемое время, начните запускать код, который вы хотите запустить при закрытии Windows…
Ответ №9:
Используйте:
<body onUnload="javascript:">
Он должен фиксировать все, кроме завершения работы программы браузера.
Комментарии:
1. хм (-1), мне было бы очень интересно узнать, что не так с этим методом. Никогда не использовал его сам, но много раз сталкивался с ним как с решением вашей конкретной проблемы.
2. Я опубликовал ответ, в котором я объясняю значение -1.