Работа с JS для проверки UX-гипотез

Для проведения A/B тестов важно уметь самостоятельно изменить верстку сервиса. UX-проектировщики регулярно сталкивались и будут сталкиваться с ситуацией, когда нет возможности привлечь для проверки гипотезы менеджера, графического дизайнера, backend и frontend разработчика, а проверить гипотезу надо. В этом случае, необходимо редактировать готовый веб-сервис самостоятельно. Многие возможности JS позволяют не только изменить поведение контролов на сайте, но и получить достаточно много интересных данных с форм ввода. Сразу хочу отметить, что от UX/IA дизайнера никто не должен требовать Production Ready кода, ваш код должен решать лишь ваши задачи.

Давайте рассмотрим, какие возможности браузера и JS нам могут быть полезны в первую очередь. Браузер состоит из DOM (либо Virtual DOM) и BOM. DOM это документ, со всеми body, div, span и прочими элементами. Структура документа, состоящая из объектов. Все свойства DOM описаны на www.w3.org. BOM — объекты для работы с чем угодно независимо от контента страницы. Для управления DOM и BOM используется JS.

С DOM все понятно, давайте посмотрим на пример работы с BOM. Если разобрать на составляющие адрес в браузерной строке https://your-scorpion.ru/portfolio#about, то

  • функция location.href вернет весь URL
  • location.hostname вернет лишь your-scorpion.ru
  • location.pathname вернет /portfolio/
  • location.hash вернет хэш #about.

 

Это был понятный, но не самый практичный пример возможностей BOM, более практичные примеры будут во второй части статьи. При проведении тестирования важно реагировать на действия пользователя, для этого используются события. Это сигналы от браузера о том, что пользователь сделал какое то действие. Назначить обработчики событий можно следующими способами:

  1. Атрибут HTML: onclick="...".
  2. Свойство: elem.onclick = function.
  3. Метод elem.addEventListener( событие, handler[, phase]).

 

Обработчики событий работают со следующими событиями:

  • click – клик по элементу левой кнопкой мыши
  • contextmenu – клик по элементу правой кнопкой мыши
  • mouseover – на элемент наведена мышь
  • mousedown и mouseup – нажали и отжали кнопку мыши
  • mousemove – любое движение мыши
  • submit – отправил форму, работает в тэге <form>
  • focus – посетитель фокусируется на элементе, например нажимает на <input>
  • keydown – когда посетитель нажимает клавишу
  • keyup – когда посетитель отпускает клавишу
  • DOMContentLoaded – когда HTML загружен и обработан, DOM документа полностью построен и доступен
  • transitionend – когда CSS-анимация завершена

Это далеко не полный список, но это основные события, используемые UX-проектировщиками и дизайнерами при проработке разных состояний контролов.

 

Примеры

<input type="button" value="Кнопка" />

Обработчик может быть назначен прямо в разметке, в атрибуте с названием on<событие>. Сам атрибут находится в двойных кавычках, поэтому для onclick используются одинарные кавычки. Писать напрямую в разметке это не лучшая практика. Обычно в разметке пишут простые обработчики для быстрых тестов. Правильнее написать свою функцию, и вызывать ее из обработчика.

<!DOCTYPE HTML>
<html>
<head>
  <meta charset="utf-8">

  <script>
    function countRabbits() {
      for(var i=1; i<=3; i++) {
        alert("Кролик номер " + i);
      }
    }
  </script>
</head>
<body>
  <input type="button" onclick="countRabbits()" value="Считать кроликов!"/>
</body>
</html>

Здесь используется вызов функции countRabbits(), сама функция написана выше. Функция это то, что умеет создавать значение. Обработчик может быть назначен и на элемент DOM. Выглядит это как on<событие>. Вот пример:

<input id="elem" type="button" value="Нажми меня" />
<script>
  elem.onclick = function() {
    alert( 'Спасибо' );
  };
</script>

Основной минус такого подхода: DOM-свойство onclick одно, и назначить более одного обработчика не получится. Мне, как Flash-разработчику в прошлом, куда ближе методы addEventListener и removeEventListener, которые являются лучшим способом назначить или удалить обработчик, и при этом позволяют использовать неограниченное количество любых обработчиков. Назначение обработчика осуществляется вызовом addEventListener, который имеет три аргумента:

element.addEventListener(event, handler[, phase]);

event — имя события. handler — ссылка на функцию, которую надо поставить обработчиком. phase не обязателен к использованию, отвечает за «место», на которой обработчик должен сработать. Не только пользовательские события могут служить триггером для отработки функции. Рассмотрим пример.

 document.addEventListener(
'DOMContentLoaded',
console.log("Я отработаль"),
{once: true}
);
//или проще
document.addEventListener("DOMContentLoaded", ready);

Это очень простой пример, в котором текст «Я отработаль» будет выведен в консоль в тот момент, когда браузер полностью загрузил HTML и построил DOM-дерево. Важно понимать, что событие «Окно загружено» сработает позже всех, т.к. происходит когда окончательно загрузится весь контент и скрипты на странице.

Во втором случае мы вызываем функцию ready. Итак, у вас есть два способа:

document.getElementById('id0').onclick = function() {
    alert('Может быть только один обработчик');
}
 
document.getElementById('id1').addEventListener('click', function(event) {
    alert('Сколько угодно обработчиков');
})

Практические примеры:

Есть тестовое поле для ввода цифр, вешаем на него код, и проверяем, что при клике на кнопку мы получим соответствующее сообщение в консоли браузера. При клике на кнопку мы получим уведомлении о совершении действия пользователем.

var button = document.querySelector('button');  //при этом у кнопки есть соответствующий тэг button
button.addEventListener('click', function() {console.log('Button clicked.');

Все работает при условии, что текстовому полю назначен id=’cecer’. Осталось вместо button clicked слать в консоль содержание поля.

button.addEventListener('click', function() {console.log(document.getElementById('cecer').value);});


Допустим, в форме заявки есть набор блоков, работающих по принципу чек-боксов. Нас интересует узнать, какие опции люди включали и в какой последовательности. Необходимо написать код, и добавить к каждому из блоков уникальный idшник. Результат и код вы можете увидеть ниже.

$('.ui-input-image__input').mouseover(function() {console.log(this.id);});

Можно получать много параметров за раз. Например, ниже представлена реализация на основе связного списка, возвращающая ширину и высоту экрана устройства, дату клика в текстовое поле (id 1) и текущий URL.

var today = new Date();
document.getElementById('1').addEventListener('click', function(event) {
    var list = {
  value: today,
  next: {
    value: screen.height,
    next: {
      value: screen.width,
      next: {
        value: window.location.href,
        next: null
      }
    }
  }
};
 
function printList(list) {
  var tmp = list;
 
  while (tmp) {
    console.log( tmp.value );
    tmp = tmp.next;
  }
}
printList(list); 
})

Безусловно, это очень простые примеры. Мне доводилось писать более 1000 строк кода для получения достаточного количества данных при проверке гипотезы. Написав код, вы добавляете его в Google Tag Manager и проводите A/B тестирование, параллельно собирая данные.

Если вы занимаетесь разработкой сервиса с нуля, то есть способ сильно облегчить свою дальнейщую жизнь. Для этого нужно продумать разметку перед передачей макетов в разработку. А именно, вас будет интересовать структура и наименование элементов верстки. Необходимо учесть категоризацию в структуре эвентов. Например, есть сценарий оформления электронной подписи, все действия в рамках этого сценария мы поместим в event-категорию order_esignature, это поможет не смешивать сценарии. Вот список минимальных требований:

  • URL-адрес страницы
  • Триггер — что инициирует получение данных
  • Category — к какому типу относится используемый компонент, Inputs, Buttons, etd
  • PageType — на какой странице или шаге мы находимся
  • Action — описание, что именно делает этот элемент (from/to, дата рождения, etd)
  • Label — значение полей или контролов

Например, Category: order_esignature, Action: esign_CTA, Label: buy_esign. Смысл в том, чтобы потом было удобно и быстро искать нужные события (и понимать к чему они). Разумеется, в рамках такого ТЗ обычно передается описание всех привязок к Google Analytics.

 

22 комментария

  1. Алексей Блохин

    Добрый день.
    Подскажите, все сервисы работают на апи, и иногда требуется делать дизайн апи. Что такое API и как это относится к проектированию интерфейсов?

    • your-scorpion (Author)

      API это своего рода ответ на вопрос, который задает программный продукт. Чаще всего существует в формате JSON (Javascript Object Notation). Вот пример API:

      Входные данные:
      {
      "consumer_id": "e654",
      "phone": "99876543210"
      }

      Выходные данные:
      {
      "id": "eo909oprv55"
      "last_name": "Иванов",
      "first_name": "Иван",
      "middle_name": "Иванович",
      "gender": "M",
      "birthed": "1982-01-01",
      "phone": "99876543210",
      "inn": "123654987456",
      }

      Как видно, это набор параметров относительно пользователя, спрятанного за неким id. В примере есть входные и выходные данные, следовательно, пользователь тоже может создавать или обновлять параметры API. Основной плюс API: нельзя намертво фиксировать в коде данные, которые могут измениться, и API помогает легко обновлять данные без обновления всего программного продукта.

      Проектировщик интерфейсов должен документировать все, что нужно для разработки. При подготовке спецификаций для разработчиков и проектировании интерфейсов важно понимать, какое у проекта API и какие возможности оно дает.

  2. Dendy Herlambang

    Добрый день.
    Подход, описанный в статье, работает только с ванильным JS, или прогрессивные фреймворки тоже необходимо учить для работы?

    • your-scorpion (Author)

      Можно и нужно использовать фреймворки, такие как Vue, React, Angular.
      Например, вот пример вывода данных в консоль на Vue.js, аналогичный тому, что я приводил в статье. При нажатии на Enter в консоль выводится содержимое текстового поля.

      <div id="app">
      		<input type="text" v-model="message" v-on:keyup.13="onclick">
      </div>
      <script src="https://unpkg.com/vue@2.1.4/dist/vue.js"></script>
      <script>
      	new Vue ({
      		el: "#app",
      		data: {
      				message:''
      		},
      		methods: {
      			onclick: function() {
      				console.log(this.message)
      			}
      	}
      })
      </script>

      Директива v-model позволяет связывать элементы формы input и textarea. Директива v-on нужна, когда мы хотим отследить, нажал ли пользователь клавишу на клавиатуре, в нашем случае это Enter. Так, с помощью keyup.13 мы отслеживаем нужную нам клавишу. И в кавычках пишем название метода, который должен отработать в случае нажатия на Enter, у нас это onClick.

      new Vue создает экземпляр класса Vue. Внутрь добавляем объект methods. И уже внутри него указываем название метода onClick, в качестве значения передавая сам метод.

      this используется для передачи одноименного свойства объекта data.
      data это функция. message это свойство объекта data.

      У клавиш есть клавиатурные сокращения, которые я использовал в примере, можете ознакомиться.

      • Pavel Ushakov

        Интересно, а почему сначала указан div id=»app», а потом script?

        • your-scorpion (Author)

          Это шаблон Vue.js. В данном примере текст передается через переменную, что позволяет его динамически менять.

          <div id="app">
            {{ message }}
           
          <script src="http://cdnjs.cloudflare.com/ajax/libs/vue/1.0.26/vue.min.js"></script>
              <script>
                  var app = new Vue({
            el: '#app',
            data: {
              message: 'Test text'
            }
          })
              </script>
          </div>

          В двойных скобочках {{}} мы создаем экземпляр Vue, внутри которого объявляем переменную message. В переменную записываем наше сообщение.

  3. Alexander Mirnyj

    Если в верстке не предусмотрены id, как и class или URL. Как тогда решать задачу отслеживания?

    • your-scorpion (Author)

      Еще можно привязываться к селекторам CSS, это иногда позволяет не вмешиваться в код сайта. Для их использования требуется выбрать Click Element или Form Element.

      Допустим, можно привязаться к селектору для кнопки. В примере ниже уникальный #gform_btn дает безошибочную привязку к нужному элементу.

      #gform_btn [type="submit"]

      В Google Tag Manager создаем новую переменную c методом CSS Selector. Вбиваем имя селектора, например #form_element .title

      Работает так: элемент с id «form_element » обращается к дочернему элементу с классом «.title». Эту переменную можно передавать как атрибут события при выполнении клика.

  4. Artem Originative

    Добрый день!
    Подскажите, пожалуйста, у меня такая задача.
    Есть контактная форма, она выводит введенные пользователем данные в текстовое окно. Как я могу забирать данные из этого окна?

    • your-scorpion (Author)

      Допустим, контактная форма такая

      <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" 
        "http://www.w3.org/TR/html4/strict.dtd">
      <html>
       <head>
         <title>!DOCTYPE</title>
         <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
       </head>
       <body>
       
      <div id="blocks">
      <label><input checked="" type="checkbox"> text1</label><input value="20"><input value="13"><br>
      <label><input type="checkbox"> text3</label><br>
      <label><input type="checkbox"> text15</label><input value="10"><br>
      <label><input checked="" type="checkbox"> text1</label><input value="20"><input value="13"><br>
      <label><input type="checkbox"> text3</label><br>
      </div>
      <textarea id="text_for_show" cols="90" rows="10" onkeypress='keypress(event)'></textarea>

      Опишем задачу. Нужно перебрать массив вложенных элементов, для этого надо создать переменную с полем, куда выводится результат, как вы описали. Собираем элементы в псевдомассив. Проверяем, если первый параметр соответствует ‘label’, то записываем в поле значение 1/0 или текст из текстовых полей.

      <script>
      	var area = document.querySelector('textarea');
      		[].forEach.call(document.querySelectorAll('#blocks>*'), 
      			function ss (text_get, perm){   
      				if(text_get.matches('label')) { 
      					area.value +=perm?' r'+(+text_get.children[0].checked):+text_get.children[0].checked; 
      					area.value += text_get.textContent; 
      				}
       
      				if(text_get.matches('input')) area.value += ' '+text_get.value; //
      		});
       
      		var inpt = document.getElementById('text_for_show');
                      var msg = inpt.value;
      		content=document.getElementById('text_for_show').innerHTML
      		console.log(msg);
      </script>

      querySelector возвращает первый элемент, соответствующий CSS-селектору textarea;
      forEach перебираем массив, далее в функции передаем два параметра, метод call собирает найденные элементы в «псевдомассив»;
      matches() проверит является ли элемент вернет ‘label’ и вернет true или false, в зависимости от того, соответствует ли элемент указаному css-селектору;
      += это присвоение со сложением;
      textContent содержит только текст внутри элемента, за вычетом всех < тегов>.

  5. Alexander Solyarskyi

    Подскажите, чем руководствоваться в следующей ситуации. Я менеджер, меня не устраивает качество работы нашего дизайнера. Мы провели A/B тест моего варианта карточки товара и варианта, который предложил дизайнер. Мой вариант выйграл. A/B тест может служить подтверждением низкой квалификации дизайнера и поводом для увольнения?

    • your-scorpion (Author)

      Начнем с того, что A/B тесты бывают двух видов: на клиенте и на сервере. На клиенте тестируют фронтенд, сервер-сайд это тест ассортиментной матрицы. При планировании теста надо руководствоваться не мелочами в дизайне, а индексом PIE Score. Мелочи в дизайне, вроде цвета кнопок или размера заголовка, очень слабо влияют на ключевые метрики продукта. И их легко протестировать с помощью MVT-тестирования. Так что A/B-тест это не проверка дизайнера, а проверка гипотезы.

      И вы уверены, что убедились в статистической значимости результатов теста? Было ли проведено пристрастие для удаления выбросов? Всегда лидировал один вариант дизайна или они чередовались? Был ли поделен трафик на параметрические доли?

  6. Andrey Vassilyev

    Можно ли менять в GTM длинные URL на какие то понятные имена?

    • your-scorpion (Author)

      Первое, что приходит на ум, это LookUp Table.
      Указываете в списке значение, которому при соответствии переменной будет присвоено выбранное вами значение. Значением может быть любая переменная GTM.

      Поля чувствительны к регистру и требуется точное совпадение.

      Если же вам требуется не точный результат, что бывает довольно часто, то обратите внимание на RegEx Table. Как работать с регулярными выражениями, я уже писал. Переменная возвращает первое совпадение. Есот оставить галочку Full Matches Only включенной, то пример dev\.infotecs\.ru будет возвращать dev.infotecs.ru. Если галочку отключить, то будет возвращено, например, anydev.infotecs.ru.com.

      Либо писать собственный JavaScript variables, как вариант для особо изощренных умов.

      • Порхай как бабочка, жаль я программист

        Не могу понять, как тестировать верстку с помощью GTM? Это инструмент для маркетологов, а не дизайнеров. В нем можно тестировать две разные версии посадочной страницы, в которых разные попапы?

        • 1. Вашу задачу я бы решил с помощью триггера «доступность элемента», обычно с его помощью отслеживают действия ajax. Сначала создаете макрос, затем триггер и в завершение уже сам тег. Выбираете попап для тестирования, цепляетесь к его селектору.


          2. Настраиваете триггер


          3. Создаете новый пользовательский HTML-тэг, в котором расположен код JS. Код должен разбивать выборку на тестируемую и контрольную страницу. Пишем код JS в тэг, в моем случае я написал простой concole.log, вы же пишете код, который нужен для теста.

          Все изменения в верстке пишутся в специальный контейнер Google Optimize.

          Этот способ не подходит для тестирования двух разных страниц, только для низкоуровневых гипотез. Появится проблема мерцания, т.к. скрипты подгрузятся не сразу. Мерцание возможно и при обычном A/B тесте, когда вы меняете фон страницы, размеры шрифтов и формы кнопок. Этого можно избежать только оптимизируя код.

  7. Razzwan Lomov

    Решил проверить отчеты в AdWords в Google Analytics, и данные по кликам расходятся. Не знаю как у вас здесь картинку прикрепить, но разница почти в два раза. Куда копать для решения проблемы?

    • Не исключено, что все настроено правильно. Существуют клики (переходы на ваш сайт по рекламе), и сеансы (заходы на сайт). Если пользователь зашел на ваш сайт несколько раз подряд за короткий промежуток времени, то будет посчитано несколько кликов и один сеанс. Либо код/тэг не успевает загрузится, так как сайт медленно грузится, или при редиректах затираются gclid или utm. Да и gclid может передаваться из одного URL в другой.

      Куда копать:
      ● Если есть мобильная версия сайта, то могли забыть добавить код Google Analytics или Tag Manager. В настройках AdWords добавьте параметр device category в secondary dimension, это позволит увидеть проблему.
      ● Часто затираются utm-метки или параметр gclid, проверяется просмотром в реальном времени собственного перехода.
      ● Нет связи с AdWords. Лечится включением нужных галочек в Adwords Linking.
      ● Если у вас много сайтов на одном аккаунте AdWords, то по некоторым сайтам данные могут не отображаться. Для проверки включите в отчете ACQUISITION -> AdWords -> Campaigns и добавьте в secondary dimension параметр Display URL.

      • Denis Kuandykov

        Добрый день, у нас проблема с UTM-метками. Посмотрите, что с ними может быть не так:
        http://www.нашсайт.com/?utm_source=yandex.ru&utm_campaign=shop_conf&utm_term=cart

        • Цветков Максим (Author)

          Всего есть три обязательных параметра для указания UTM-метки.
          Utm_source — для источника трафика.
          Utm_medium — тип источника трафика.
          Utm_campaign — название рекламной компании, в составе которой работает рекламное объявление.

          У вас отсутствует тип источника трафика. А вот использованный вами Utm_term отвечает за ключевые слова и не является обаятельным. Я предположу, что метки вы расставляли вручную, а лучше это делать с помощью специальных сервисов.

  8. Igor cloobok

    Спасибо за статью, самое оно! Подскажите, а есть ли простой способ отслеживать нажатия клавиш с клавиатуры и менять UI клиента в зависимости от нажатых клавиш? Допустим, показывать или прятать подсказку под текстовым полем или сразу прятать все иконки на сайте.

    • Цветков Максим (Author)

      Да вариантов то полно, например через keyCode. Указываете диапазон клавиш или определенные кнопки клавиатуры, отслеживаете нажатие и меняете UI обычным if/else и CSS свойствами.

      kitinput.addEventListener('keydown', returnKey, true); //kitinput это id целевого текстового поля 
       
      function returnKey(x) {   
          var x = x.which || x.keyCode || 0;
          if (x == 97 || x == 98)   { //клавищи на клавиатуре 1 и 2 (numpad)
              console.log ("попытка набрать 1 или 2");
              document.getElementById('kitinput_notify').style.visibility = "hidden"; //kitinput_notify это id элемента, который будем прятать
          }
          else{
          document.getElementById('kitinput_notify').style.visibility = "visible";
        }
      }

      В примере выше по нажатию на клавиши 1 и 2 с numpad прячется элемент. Конечно, кейс несколько оторван от реальности, но принцип должен быть понятен.


      Если кроме иконок на странице нет графики, то можно попробовать такой код

      kitinput.addEventListener('keydown', returnKey, true); //kitinput это id целевого текстового поля 
       
      function returnKey(x) {   
          var x = x.which || x.keyCode || 0;
          if (x == 97 || x == 98)   { //клавищи на клавиатуре 1 и 2 (numpad)
              for(var i = 0; i < document.images.length; i++) 
            document.images[i].style.visibility = "hidden";
          }
       
          else{
            for(var i = 0; i < document.images.length; i++) 
            document.images[i].style.visibility = "visible"; 
        }
      }

«Нажимая на кнопку Submit Comment, я даю согласие на обработку персональных данных»