Постой паровоз, или call-to-action перед потерей клиента
Я думаю все вы видели эту маркетинговую хитрость - показать привлекательный оффер пользователю перед его уходом. В этой статье рассмотрим простой способ реализации этого хода.
Честно говоря, мне не очень нравится заниматься разработкой лендингов, ведь по сути своей, очень мало чего приходится реализовывать на таких страницах. В основной своей массе - это просто красивая обертка, набор плагинов и простенький mail скрипт. Однако, и в этой сфере встречаются довольно интересные кейсы для разработчика.
Одним из таких кейсов является реализация маркетинговой фишки - остановка юзера от ухода, путем показа спецпредложения в последний момент.
Задавшись вопросом "как это сделать", Вы вероятно предложите использовать метод onunload, однако во-первых этот метод способен выдать только системное окно, и если пользователь не желает оставаться страница все равно будет закрыта, и во-вторых, похоже что, современные браузеры начали игнорировать его.
Что делать-то?
Так как мы не можем явно определить момент закрытия вкладки (а даже если и можем - не можем повлиять на это), то остается только надеяться на поведенческий фактор наших юзеров.
Мы собираемся отслеживать последние N координат курсора мыши внутри страницы и записывать их в динамической очереди фиксированного размера, и как только курсор уходит из предела видимости - просчитываем направление. Зная направление курсора, остается подождать Y миллисекунд и запустить свой код.
При этом, если курсор вернется в область видимости до того как пройдет Y мс от начала события, нужно отменить вызов отложенного кода и продолжить сбор информации о позиции курсора.
Динамическая очередь?! Фиксированного размера??!! WTF?
Из соображений производительности и экономного расхода ОЗУ, было бы безответственно хранить координаты курсора в обычном массиве или объекте. Из-за того что мышь при малейшем движении стреляет событием с координатами, у нас мог бы получиться просто колоссальный массив данных и просчет всего пути курсора занял бы возмутительно много времени. (В нашем случае сложность - O(n), при использовании простого подсчета очков, т.е. время обработки напрямую зависит от количества информации).
Кстати, что я тут называю динамической очередью фиксированного размера, вообще-то называется кольцевым буфером (вики)
Поэтому я реализовал т.н. динамическую очередь фиксированного размера. Это самая обыкновенная очередь (первым вошел, первым вышел), но имеющая определенный размер, и в случае переполнения автоматически выталкивающая первый элемент перед помещением нового.
Таким образом, в любой момент времени у нас будет храниться не больше заданного количества элементов. Приступим к реализации:
/* конструктор очереди */
function fixedSizeQueue(size) {
/* базируемся на объекте массива */
var queue = Array.apply(null, []);
queue.fixedSize = size;
queue.trimHead = fixedSizeQueue.trimHead;
queue.push = fixedSizeQueue.push;
return queue;
}
/* усечение длины очереди */
fixedSizeQueue.trimHead = function() {
// если переполнения нет, то и усекать массив не нужно
if(this.length <= this.fixedSize)
return;
Array.prototype.splice.call(this, 0, (this.length - this.fixedSize));
}
/* запихивание элемента в очередь */
fixedSizeQueue.push = function() {
var result;
// заталкиваем элемент в массив
result = Array.prototype.push.apply(this, arguments);
// и усекаем
this.trimHead(this);
return result;
}
Вот и вся реализация! Я думаю, у вас не должно возникнуть проблем с пониманием того как это работает.
ОК, а что делать с данными?
Теперь, когда у нас есть очередь, мы можем приступить к реализации функции оценки действий пользователя. В таких проектах не требуется предельная точность расчетов, я решил написать несложную функцию оценки на основе подсчета очков.
Нам интересно - двигался ли курсор вверх или вниз, таким образом мы будем присуждать очки состояниям up и down, в результате проверки координат:
function whereDidHeGo(coords) {
var scores = {
up : 0,
down : 0
}
, i = 0
, len = coords.length;
for(; i < len - 1; i++) {
if(coords[i] > coords[i+1])
scores.up++;
else
scores.down++;
}
return scores.up > scores.down ? 'up' : 'down';
}
Связываем все вместе
Для определения направления курсора, нам будет вполне достаточно 20 последних позиций курсора. Но тут есть одно НО. Из-за того что мышь генерирует события слишком часто, мы можем получить не корректные данные, поэтому запись координат следует делать через определенные промежутки времени. Определим все это в коде:
var storage = fixedSizeQueue(20) // хранилище координат
, lastTick = Date.now() // текущее время
, tickDelay = 10; // время между записью позиции
А теперь добавим реализацию захвата позиций курсора:
$('body').on('mousemove', function(e) {
var nowIs = Date.now();
// если прошло больше чем tickDelay времени - проталкиваем координаты
if((+lastTick) + (+tickDelay) < (+nowIs)) {
storage.push(e.clientY);
lastTick = nowIs;
}
});
Отлично! Теперь в нашу очередь через определенные промежутки времени будут попадать координаты курсора по оси Y.
Теперь осталось лишь написать управляющий код:
// переменная для объекта таймера
var $outDelay = null
, navigateToClose = 432; /* приблизительное время довода курсора до кнопки закрытия */;
$('body')
.on('mouseleave', function(e) {
// если курсор ушел с <body> - ждем navigateToCloseBtnTime мс и выполняем код
$outDelay = setTimeout(function() {
var direction = whereDidHeGo(storage);
if(direction == 'up') {
// ЗДЕСЬ КОД КОТОРЫЙ БУДЕТ ВЫЗВАН ПРИМЕРНО ПЕРЕД ЗАКРЫТИЕМ ВКЛАДКИ
// выключаем события отлова координат
$('body').off('mouseleave mouseenter');
alert('Подождите! У нас есть для вас оффер!');
}
}, navigateToCloseBtnTime)
})
.on('mouseenter', function() {
// если курсор вернулся до таймаута сбрасываем таймаут, т.о. продолжаем отлов координат
clearTimeout($outDelay);
});
Вот и все! Теперь вы без проблем сможете определять уход пользователя со страницы. Конечно, есть ситуации когда пользователь увел курсор для переключения вкладок, однако, я пока что не нашел способов отследить это поведение.