S.Tominoff

Fullstack JavaScript разработчик

Постой паровоз, или 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);
    });

 

Вот и все! Теперь вы без проблем сможете определять уход пользователя со страницы. Конечно, есть ситуации когда пользователь увел курсор для переключения вкладок, однако, я пока что не нашел способов отследить это поведение.