Chrome Debugger: инструмент, который ты зря игнорируешь
Обычно мы дебажим с помощью console.log. Признаюсь, я использую console.log при дебаге в 99% случаев, но когда ситуация становится слишком сложной, одних console.log’ов уже не хватает. В такие моменты нужно замедлиться, шаг за шагом пройтись по коду и разобрать что же в нём происходит. И дебаггер может нам с этим помочь.
Что такое дебаггер и для чего он нужен?
Дебаггер (или отладчик на русском) — это инструмент, с помощью которого можно остановить выполнение JavaScript на нужной строке кода. Контролы дебаггера позволяют выполнять код построчно и видеть, что происходит после выполнения каждой строки.
Чтобы открыть дебаггер, перейди на панель “Sources” в DevTools.
Эта панель разделена на 3 части:
- Слева отрыта вкладка “Page” с деревом файлов. Все эти файлы были загружены при загрузке приложения.
- В центре редактор кода. Здесь отображается содержимое выбранного файла.
- Справа — всё, что связано с дебаггером. Позже разберём, что там есть.
Первый способ запустить дебаггер — добавить в код выражение debugger; на строке, где нужно остановить выполнение JavaScript.
Давай добавим его в код и запустим дебаггер.
Дебаггер запустился, выполнение скрипта остановилось, и на странице появился оверлей с двумя кнопками:
- Первая - продолжает выполнение скрипта,
- Вторая - пропускает следующий вызов функции.
Это самые часто используемые контролы навигации, поэтому их вынесли на оверлей.
Полный список контролов находится в правом верхнем углу DevTools. Давай разберём, что делают эти кнопки.
Навигация
Первая кнопка продолжает выполнение скрипта. Если во время выполнения встретится брейкпоинт, дебаггер остановится на этой строке. Брейкпоинт — это строка или место в коде, где мы говорим дебаггеру остановить выполнение. Один из видов брейкпоинтов — выражение debugger, которое мы уже использовали. Остальные виды брейкпоинтов разберём чуть позже.
Если зажать левую кнопку мыши, то появятся ещё две скрытые кнопки.
Кнопка Play продолжает выполнение скрипта и выключает все брейпоинты на 500 мс.
Это удобно, когда у тебя много брейкпоинтов, и ты хочешь выполнить скрипт без остановок.
Кнопка Stop выходит из режима дебаггера без продолжения выполнения скрипта. Используй её, если знаешь, что дальше скрипт изменит приложение, а тебе не хочется потом всё откатывать.
Например, код добавляет флаг в LocalStorage, а для дебага нужно приложение без него. Поэтому каждый раз когда скрипт выполнился тебе нужно вручную удалять этот флаг из LocalStorage. Чтобы этого избежать, можно не выполнять скрипт до конца — тогда код, который добавляет флаг, не сработает.
Следующая кнопка “Step over next function call” выполняет код на текущей строке и если это вызов функции, то дебаггер не зайдёт внутрь функции, а перейдёт на следующую строку. На оверлее дебаггера мы уже видели эту кнопку.
Кнопка “Step” похожа на предыдущую, но если дебаггер остановился на вызове функции, то при клике он заходит внутрь и останавливается на первой строке тела функции.
Для асинхронного кода используй кнопку “Step into”. Она работает как “Step”, но также заходит внутрь асинхронных функций. Это полезно при дебаге setTimeout, промисов или пересылки сообщений между воркером и основным потоком.
Последняя кнопка — “Step out of current function”. Она выводит дебаггер из текущей функции. Это удобно, когда в функции уже нечего дебажить, и ты хочешь сразу из неё выйти.
Про эту кнопку мы поговорим чуть позже.
Редактор кода
Пока выполнение скрипта остановлено дебаггер вычисляет значения переменных, выражений и функций.
Интересно, что все эти переменные и функции доступны и в консоли. Например, можно вывести в консоле переменную buttons и использовать её как любую другую переменную определённую в консоли.
Во время дебага часто нужно менять код, и это можно делать прямо в браузере. Нажми правой кнопкой в любое место редактора кода и выбери “Override content”. Это создаст копию файла, в которую можно вносить изменения. Браузер будет использовать её вместо оригинала. Чтобы это заработало, нужно выбрать папку для сохранения переопределённых файлов.
Давай добавим console.log('Этот файл переопределён');, сохраним файл и перезагрузим страницу. Добавленный console.log выполнился и сообщение отобразилось в консоли.
Это может сильно ускорить процесс дебага, когда тебе нужно делать много изменений в коде и ты не хочешь постоянно переключаться в редактор кода чтобы их внести. Переопределять можно и локальные файлы, и файлы в продакшене.
В Chrome можно не только переопределять файлы, но и сразу править исходники. Для этого открой вкладку Workspace в левой панели, выбери папку проекта и файл который ты хочешь отредактировать.
Давай добавим console.log('Правки внесённые в Workspace');, сохраним и перезагрузим страницу. Как видишь, сообщение появилось в консоли — значит изменения применились. Давай откроем VSCode и проверим изменились ли исходники. Как видим внесённая правка сохранилась в исходниках.
Но самый удобный способ — это дебажить и править код прямо в редакторе кода. Это тема для отдельного видео и если тебе интересно как я пользуюсь дебаггером в VSCode, напиши про это в комментариях и я сниму про это видео.
Watch
В секции “Watch” можно добавить любое валидное JavaScript-выражение, и оно будет вычисляться во время дебага. Это позволяет видеть результат выражений без console.log и отслеживать, как меняются их значения во время выполнения кода.
Это похоже на Live-выражения в консоли.
Давай добавим переменную buttons и запустим дебаггер. Сейчас buttons недоступна, потому что она объявлена дальше в коде. Когда дебаггер выполнит строку где определена переменная, отслеживаемое значение обновится.
Scope
В секции Scope показаны все области видимости в текущем положении дебаггера и значения переменных внутри них.
Например, сейчас есть:
2 блочных области видимости:
- первая формируется внутри
catchблока, в ней есть только переменнаяblockVarInCatchBlock - вторая формируется
ifблоком и содержит переменнуюblockVar
Между ними — область видимости, сформированная переменной error в catch. Chrome создаёт для неё отдельный scope.
Дальше идёт локальная область видимости, созданная функцией createLocalScope, внутри которой сейчас находится дебаггер.
Область видимости модуля формируется внутри ESM-модуля — файла, из которого что-то экспортируется или в который что-то импортируется.
Дальше идёт область видимости скрипта. В неё попадают все переменные определённые на верхнем уровне из inline-скриптов и подключённых скриптов. Переменные определённые в модуле не будут доступны в этой области видимости. Сейчас ты видишь переменные, определённые в другом скрипте на странице.
Последней всегда идёт глобальная область видимости со всеми свойствами глобального объекта. В зависимости от платформы глобальный объект будет разные: в браузере будет Window, в Worker - WorkerGlobalScope, в Service Worker - ServiceWorkerGlobalScope, а в NodeJS - Global.
Breakpoints
В секции Breakpoints есть два чекбокса — о них поговорим чуть позже.
Под ними пока пусто, но здесь появятся все добавленные брейкпоинты. Давай добавим первый брейкпоинт. Для этого нажми в редакторе на номер строки, где нужно остановить выполнение. Этот брейкпоинт сработает так же как если бы ты добавил на эту строку выражение debugger.
Сейчас ты видишь его в списке активных брейпоинтов. Нажав на чекбокс ты можешь его отлючить. Нажав на текст брейпоинта ты перейдёшь на строку где брейпоинт был добавлен.
Чтобы удалить брейпоинт, нажми на иконку брейкоинта рядом с номером строки.
Нажав правой кнопкой мыши на номер строки откроется контекстное меню в котором можно добавить ещё 2 вида брейпоинтов:
- conditional breakpoint и
- logpoint
Давай добавим их.
Conditional breakpoint
Conditional breakpoint останавливает выполнение JavaScript только если указанное в нём условие равно true. В условии может быть любое валидное выражение.
Для примера, я добавил 2 conditional брейкпоинта, и в условие первого указал true, а в условии второго — false. В итоге выполнение кода остановится на первом брейкпоинте и не остановится на втором.
Logpoint
Logpoint выводит результат выражения в консоль.
Например, можно вывести значение переменной buttons.
Logpoint не останавливает выполнение JavaScript, а просто выводит сообщение в консоль. Он работает так же, как console.log, а чтобы отличить его от console.log, в начале сообщения в консоли есть иконка Logpoint.
Основное его преимущество над console.log это то что тебе не нужно редактировать код чтобы его добавить, а значит не нужно пересобирать приложение. Это быстрее и Logpoint можно добавить к любому скрипту в браузере, даже в продакшене.
Каждый брейкпоинт имеет свой цвет. У Conditional breakpoints и Logpoints в списке брейкпоинтов есть цветная плашка, которая помогает различать их между собой.
Иногда при добавлении брейкпоинта можно увидeть в коде на той же строке пустой маркер брейкпоинта. Это значит, что туда можно добавить ещё один брейкпоинт. Нажми правой кнопкой мыши на него и выбери тип брейкпоинта который ты хочешь добавить.
On exceptions
Вернёмся к 2 чекбоксам в начале секции Breakpoints. Они останавливают выполнение скрипта при ошибках или reject промисов.
Pause on uncaught exceptions запускает дебаггер на каждом необработанном исключении. Мне этот брейкоинт помогал когда запрос за JSON возвращал пустую строку или HTML и при парсинге ответа возникало исключение. Так же он может быть полезно при дебаге исключений в сторонних библиотеках.
Pause on caught exceptions запускает дебаггер на строке, которая бросает исключение, которое дальше будет обработано. Это может помочь для поиска мест где ошибки были обработаны не правильно, например просто скрыты, чтобы не мешали в консоли. Так как дебаггер срабатывает на каждое обработанное исключение, он может быть слишком шумным. Позже я покажу, как с этим можно работать.
Эти брейкпоинты показывают не только место, но и условия, при которых возникло исключение. Тебе не придётся искать значения переменных — дебаггер запустится в тот момент, когда возникает ошибка, и все нужные значения будут перед глазами.
XHR/fetch
В секции XHR/fetch breakpoints можно добавить брейкпоинт на запросы. При добавлении брейкпоинта ты указываешь строку, и если она встречается в URL на которые делается запрос, дебаггер остановится в месте выполнения запроса.
Например, можно запустить дебаггер на все запросы в Google Analytics. Для этого укажи строку “google” в брейкпоинте и перезагрузи страницу.
С помощью такого брейкпоинта легко найти место, где выполняется запрос с нужными query-параметрами.
DOM Breakpoints
Следующий вид брейкпоинтов - это DOM брейкпоинты. Они запускают дебаггер, когда в DOM происходят изменения. Чтобы добавить брейкпоинт, открой панель Elements, нажми правой кнопкой на элемент, выбери Break on и нужный тип брейкпоинта:
“Subtree modifications” запускает дебаггер, когда изменяется дочерний элемент: текст, структура или порядок элементов, но он не срабатывает на удаление дочерних элементов.
Давай добавим его к этому элементу и изменим текст в дочернем элементе. С помощью $0 получим ссылку на выбранный элемент и присвоим свойству textContent новый текст.
Дальше идёт “Attribute modification” брейкпоинт. Он запускает дебаггер при изменении атрибутов элемента. Например, при переключении темы меняется дата-атрибут у элемента html. Чтобы найти, где это происходит, добавь “Attribute modification” брейкпоинт к html и переключи тему. Дебаггер запустится на нужной строке.
Следующий — “Node removal” breakpoint. Он запускает дебаггер при удалении элемента. Давай добавим его на кнопку смены темы и раскомментируем код, который её удаляет. Дебаггер остановится на этой строке. Если брейкпоинт не сработал, попробуй переоткрыть DevTools и добавить его снова.
Все DOM бейкпоинты отображаются во вкладке “DOM breakpoints” на панели “Elements” или в секции “DOM breakpoints” на панеле “Sources”. При клинке на элемент тебя перекинет к этому элементу на панеле “Elements”.
Удалить DOM брейкпоинт можно там же где ты его добавлял нажав на него ещё раз. Или кликнув правой кнопкой мыши на брейпоинт на панеле Sources и выбрав Remove breakpoint. Там же можно выключить брейкпоинт с помощью чекбокса рядом с ним.
Важный момент – DOM бейкпоинты срабатывают только на изменения через JavaScript. При изменении элементов вручную брейкпоинты тригерится не будут.
Global Listeners
В секции “Global Listeners” показаны все обработчики событий, прикреплённые к глобальному объекту. В браузере это window.
Узнать обработчики можно и другим способом — выполнить в консоли getEventListeners(window). Эта функция работает только в консоли Chrome и не будет работать из кода.
Event listener breakpoints
Дальше идёт “Event Listener breakpoints”. Здесь можно выбрать события или группы событий, при которых дебаггер будет запускаться на обработчике.
Например, выберем событие “click” и нажмём на кнопку смены темы. Дебаггер остановился на первой строчке обработчика клика этой кнопки.
CSP violation Breakpoints
“Content Security Policy (CSP) Violation Breakpoints” останавливают выполнение скрипта при нарушении политики безопасности. Они помогают защищать код от потенциальных XSS-атак (cross-site scripting). Эти брейкпоинты будут работать только если в проекте настроен CSP.
Function breakpoint
Последний вид брейкпоинтов — “Function breakpoint”. Он добавляется на вызов определённой функции. Чтобы его добавить, вызови в консоли функцию debug() и передай ей в аргументе функцию, которую хочешь отслеживать. Это работает только в консоли — из кода вызвать эту функцию не получится.
Мне не приходилось использовал этот брейкпоинт на практике. Если у тебя были такие случаи — расскажи в комментариях, будет интересно узнать про реальные юзкейсы.
Игнорирование брейпонтив и скриптов
Мы разобрали, как останавливать выполнение кода с помощью брейкпоинтов. Теперь давай посмотрим, как:
- выключить все брейкпоинты, когда они не нужны;
- игнорировать скрипты, в которых не нужно, чтобы дебаггер останавливался;
- указать дебаггеру места, где не нужно останавливаться.
Чтобы выключить все брейкпоинты, нажми на кнопку с иконкой перечёркнутого брейкпоинта на панели навигации. Это удобно, когда у тебя несколько брейкпоинтов, а нужно выполнить скрипт без остановок. Не придётся отключать каждый брейкпоинт вручную — можно их отключить одной кнопкой.
Если дебаггер срабатывает на строках, которые тебе не важны, можно подсказать ему там не останавливаться. Для этого нажми правой кнопкой мыши на строку которую ты хочешь проигнорировать и выбери “Never pause here”. В это место добавится conditional бейкпоинт со значением false в условии.
Помнишь, мы говорили про брейкпоинт на обработанное исключение и я говорил, что он может быть очень “шумным” из-за слишком частого срабатывания? Так вот, с помощью “Never pause here” можно игнорировать лишние срабатывания и уменьшить “шум”.
Если не хочешь, чтобы дебаггер останавливался в конкретном скрипте, добавь его в ignore list.
Нажми правой кнопкой мыши на любое место в редакторе кода и выбери “Add script to ignore list”. Отредактировать ignore list можно в настройках DevTools, во вкладке “Ignore list”.
Скрипт добавится в ignore list в виде регулярного выражения. Так что ты всегда можешь его отредактировать так чтобы игнорировать сразу несколько скриптов.
Call Stack
В “Call Stack” отображаются функции, которые выполняются в данный момент. Порядок функций в стеке показывает, когда они были вызваны: сверху — последняя вызванная (она завершится первой), снизу — самая ранняя (она завершится последней).
Слева от название функции есть иконка стрелки, это указатель внутри какой функции находится дебаггер, а справа указан файл и номер строки где остановился дебагер внутри функции.
Клик по функции переносит дебаггер внутрь неё.
Если нужно перезапустить выполнение функции (а не всего скрипта), нажми на неё правой кнопкой мыши и выбери Restart frame.
Асинхронных функций, генераторы или функции WebAssembly перезапустить не получится.
При отладке асинхронного кода в “Call Stack” появляются текстовые разделители между функциями. В функции ниже разделителя не получится переместить дебаггер при клике на них.
Threads
Осталась последняя секция со списком потоков которые есть в приложении. Сейчас она скрыта, потому что в приложении только один поток — главный. Чтобы разобрать эту секцию, давай переключимся на приложение с несколькими потоками. В другой вкладке у меня запущен YouTube, давай перейдём туда.
Здесь видно два активных потока: Main и sw.js — поток сервис-воркера. Слева от потока есть иконка стрелки, показывающая, что дебаггер сейчас в нём. При клике на поток дебаггер перемещается в него.
Давай остановим дебаггер в сервис-воркере. Справа от названия потока появился статус paused, так как мы его остановили. Глобальный объект теперь — ServiceWorkerGlobalScope.
Continue to here
Для того чтобы быстрее переместить дебаггер к нужной строке ты можешь нажать правой кнопкой мыши на номер этой строки и выбрать “Continue to here” и дебаггер переместиться на эту строку. Если до этой строки есть брейкпоинты, дебаггер остановится на первом из них.
Есть ещё один способ ускорить перемещение.
Зажми Command если у тебя macOS (или СTRL если у тебя Windows/Linux) и в редакторе кода подсветятся все места куда можно переместить дебаггер. При клике на подсвеченное место дебаггер переместится к нему, а если до этой строки будут брейкпоинты, он остановится на первом из них.