В этой статье я расскажу, как мы используем стандартные браузерные инструменты разработчика для эффективной отладки и исследования. Эти рецепты направлены в первую очередь на изучение приложения снаружи-внутрь, поэтому подойдут для любого проекта.
Проблема
Динамика разработки не позволяет участникам команды полностью погрузиться в детали задач друг друга. Контекст конкретного компонента быстро ускользает; вернувшись к участку кода спустя месяц, можно его не узнать. Кроме того, команда постоянно пополняется новобранцами.
В такой ситуации необходимо быстро восстанавливать понимание логики происходящего в коде. С учетом изменчивости проекта, эта задача носит скорее исследовательский характер. Понимая общие принципы работы, ты все равно каждый раз становишься первооткрывателем конкретных аспектов реализации.
Отладка
Цена за скорость разработки — наличие багов. Этот класс задач требует быстрого реагирования, а значит и входа в контекст происходящего.
Неверный результат на выходе
Часто баг проявляется в каком-то некорректном внешнем событии: непредвиденное изменение DOM-дерева, асинхронный запрос с ошибочными данными и т.д. В этом случае удобно рассматривать код приложения как черный ящик, вход которого — сценарий использования, а выход — результат бага.
Помимо стандартных брейкпоинтов на строчке кода, в DevTools (здесь и далее речь идет об инструментах браузера Google Chrome) есть возможность остановить выполнение по определенному событию.
DOM Breakpoint устанавливается на узел дерева в инспекторе. Остановиться можно при удалении этого элемента, изменении его поддерева или атрибутов (напомню, что style и class — это тоже атрибуты).
XHR Breakpoint устанавливается на весь документ и позволяет найти строчку кода, из которой посылается подпадающий под заданный паттерн запрос.
Эти брэйкпоинты отлично работают в связке с асинхронным режимом стэка вызовов (Async сall stack). Он не обрывается на асинхронных операциях и дает возможность, например, перейти из обработчика setTimeout к коду, который его установил. Таким образом, можно заглянуть намного дальше в историю и найти корни даже сложных багов.
Пример сценария:
1. Происходит непредвиденное изменение в DOM-дереве.
2. Поведение воспроизводится.
3. Устанавливаем нужный тип DOM-брэйкпоинта.
4. Включаем Async режим в отладчике.
5. Воспроизводим баг и путешествуем по истории вызовов, пока не найдем корни проблемы.
Неправильное внутреннее представление
Не все баги заметны невооруженным глазом. В ходе выполнения может меняться только внутреннее состояние, которое уже позже повлияет на поведение системы в целом. Отлавливать некорректные изменения этого состояния можно, используя воображение.
Предположим, что состояние — это глобальный объект. Тогда для слежки за ним можно использовать следующий код:
var watchMe = {};
Object.observe(watchMe, function() {
debugger;
});
watchMe.foo = ‘bar’; // Выполнение остановится
Используя продвинутые возможности консоли (о которых подробно рассказано далее), можно добавить логирование изменений, не останавливая выполнение на каждый чих.
var watchMe = {};
Object.observe(watchMe, function(options) {
options.forEach(function(option) {
var groupName = option.name + ' changed';
console.groupCollapsed(groupName);
console.log('Old value: ', option.oldValue);
console.log('New value: ', option.object[option.name]);
console.groupEnd(groupName);
});
});
Этот пример будет выводить консоль Chrome компактные группы логов при изменении свойств объекта. Кода теперь намного больше, и каждый раз писать его по памяти не удобно. Поэтому можно сохранить его как сниппет и запускать по необходимости.
Конечно, этот код придется каждый раз адаптировать к проекту и задаче. Редко все состояние хранится в одном глобальном объекте. Иногда придется редактировать исходники чтобы вклиниться в контекст выполнения. Но польза от такого подхода стоит прилагаемых усилий.
Сценарий использования:
1. Если наблюдаемый объект глобальный, то просто запускаем на нем сниппет.
2. Если объект доступен только в локальной области видимости, добавляем нужный код в приложение на время отладки.
Исследование
Работа программиста не ограничивается исправлением багов. Для добавления новой функциональности важно понимание работы приложения в целом на сложных сценариях.
Console как источник знаний
Консоль в DevTools — это не только способ быстро выполнить небольшой скрипт. Она обладает мощным API, реализующим недоступные в языке удобные функции и связки с другими инструментами DevTools.
Например, чтобы вывести в консоль DOM-элемент, не обязательно использовать сложные селекторы. Вместо этого в рамках консоли реализован стэк выделенных в инспекторе элементов. Доступ к нему происходит через команду $N, где N — отступ от конца стэка. Таким образом, обращение к $0 в консоли вернет последний выбранный в инспекторе DOM-узел, $1 — предпоследний и так далее.
Использовать эту возможность удобно в связке с функциями над DOM-узлами:
— monitorEvents(object) — следит за событиями элемента;
— getEventListeners(object) — выводит в консоль список обработчиков событий элемента, из которого можно перейти к коду функции.
Вырисовывается простой сценарий:
1. Выделяем элементы в инспекторе.
2. Вызываем в консоли нужную команду, аргументом передаем $0.
3. Радуемся, как дети.
Многие разработчики удаляют из кода console.log() сразу по окончании отладки. Но некоторая ключевая функциональность требует постоянного логирования. В результате каждый разработчик сначала пишет отладочный код, а потом его удаляет.
Есть способ не допустить вывод в консоль на продакшене и в то же время удобно логировать происходящее на этапе разработки. Мы используем UglifyJS для сжатия JavaScript, и в нем есть опция переопределения значений глобальных переменных.
// Конфиг UglifyJS
global_defs: {
DEBUG: false
}
// Где-то в коде приложения
if (DEBUG) {
console.log(‘Что-то важное’);
}
В примере выше DEBUG не просто будет равен false после сборки. UglifyJS поймет, что условие никогда не выполнится, и просто удалит его из кода.
Теперь можно использовать более сложные возможности вывода в консоль для разных типов данных. В примере с отладкой состояния вывод будет более компактным за счет console.groupCollapsed. Это используется в нашем проекте для отслеживания изменения URL:
Для массивов хорошо подходит console.table. Полный список вариантов оформления вывода находится здесь.
С высоты птичьего полета
Встроенный инструмент записи событий уровня браузера — Timeline. До недавнего времени он мог использоваться для анализа производительности приложения. С появлением экспериментальных функций, его возможности значительно расширились.
Одна из долгожданных фич — отображение стэков вызовов прямо в диаграмме событий. По сути это объединяет классический Timeline с отчетом CPU Profiler. Такая диаграмма дает понимание, как приложение работает в динамике: последовательности вызовов, общее время выполнения функций, точки взаимодействия JavaScript с DOM. Даже когда необходимости в оптимизации нет, его можно использовать для изучения внутреннего устройства проекта.
Если интересен конкретный участок логики приложения, а событий в отчете слишком много, можно воспользоваться console.time. Помимо замера времени выполнения этот метод добавляет метку в Timeline.
Пример использования:
1. Записываем пользовательский сценарий.
2. Изучаем стэки вызовов.
3. По необходимости переходим к конкретной строчке кода прямо из Timeline.
4. Если информации слишком много, оборачиваем интересующий код в console.time и console.timeEnd.
5. Повторяем до полного понимания логики происходящего.
Заключение
При работе над большими проектами имеет смысл инвестировать время в удобство разработки. Часы, потраченные на изучение и адаптацию инструментов, экономят дни отладки и дают лучшее понимание работы проекта.
В этой статье описана только малая часть возможностей Chrome Developer Tools. В работе над 2ГИС Онлайн у нас возникают и более экзотические задачи, требующие постоянного изучения новых средств разработки. Более подробно об этих инструментах, а также о производительности приложений на JavaScript, я рассказывал на конференции FrontTalks.