[Перевод] Исправление странной ошибки и стратегии отладки, проверенные временем
Помните, когда вам в последний раз доводилось сталкиваться с ошибкой, связанной с пользовательским интерфейсом, на исправление которой у вас уходили многие часы? Возможно, эта ошибка происходила периодически, без каких-то видимых причин. Может быть, она появлялась при определённых условиях (это могло зависеть от устройства, операционной системы, браузера или от действий пользователя) или была скрыта где-то в недрах одной из множества фронтенд-технологий, являющихся частью клиентской части веб-проекта?
Недавно мне пришлось вспомнить о том, какими запутанными могут быть причины возникновения ошибок пользовательских интерфейсов. А именно, речь идёт об исправлении интересной ошибки, влияющей на вывод SVG-изображений в браузере Safari. Эта ошибка возникала без какой-то определённой системы и без каких-то очевидных причин. Я, столкнувшись с проблемой, попытался найти похожие случаи, надеясь на то, что описания таких случаев дадут мне намёк на то, что происходит. Но мне не удалось отыскать ничего полезного. Правда, несмотря на все стоящие передо мной препятствия, я смог с этой ошибкой справиться.
Я проанализировал проблему, используя некоторые стратегии отладки, о которых я собираюсь рассказать в этой статье. После того, как от ошибки я избавился, я вспомнил совет, который Крис Койер дал читателям своего Твиттера несколько лет назад. Этот совет звучит так: «Напишите статью, которую вы хотели бы найти, когда обращались к поисковику». Собственно говоря, так я и поступил.
Обзор проблемы
В проекте, над которым я работал, на действующем сайте, я нашёл ошибку, проявление которой записал на этом видео. Вот как выглядит кнопка в нормальном состоянии.
Кнопка в нормальном состоянии
А вот — та же кнопка после возникновения проблемы.
Часть кнопки обрезана
Я воспроизводил эту ошибку в различных ситуациях, вызывающих перерисовку страницы. Например, она возникала при изменении размеров окна браузера.
Для того чтоб продемонстрировать проблему, я создал пример на CodePen. Подробнее его мы обсудим ниже. Но вы можете самостоятельно с этим примером поэкспериментировать. А именно, речь идёт о том, что если открыть этот пример в браузере Safari, то, при загрузке страницы, кнопки будут выглядеть так, как ожидается. Но если щёлкнуть по одной из двух более крупных кнопок, ошибка высунет свою уродливую голову.
Почему SVG-изображение оказывается обрезанным?
Каждый раз, когда происходит событие paint
, SVG-изображения, используемые в более крупных кнопках, выводятся неправильно. Эти изображения просто подвергаются обрезке. Это может происходить, без видимых причин, при загрузке страницы. Это может случиться даже при изменении размеров окна. В общем, ошибка появляется в самых разных ситуациях.
Обзор проекта, в котором происходила ошибка
Я так думаю, что, рассказывая об ошибке, хорошо будет раскрыть подробности о проекте и об условиях, в которых она происходит.
- В проекте используется React (но читателю этой статьи необязательно знать React для того чтобы в ней разобраться).
- SVG-изображения импортируются в проект в виде React-компонентов и встраиваются в HTML средствами webpack.
- Изображения экспортируются из дизайнерской программы. В их коде нет синтаксических ошибок.
- Изображения стилизуются средствами CSS.
- Изображения, которые выводятся с ошибкой, расположены внутри HTML-элемента
.
- Проблема появляется только в браузере Safari (она была замечена в его 13 версии).
Исследование ошибки
Давайте рассмотрим ошибку и поразмыслим о том, можно ли сделать какие-то предположения относительно того, что происходит. Причины подобных ошибок обычно не лежат где-то на поверхности, поэтому, столкнувшись с такими ошибками, нельзя сразу с уверенностью сказать о том, что происходит. Предпринимая первую попытку разобраться в проблеме, мы не обязаны стремиться к выявлению её причины со 100% точностью. Мы будем исследовать ошибку пошагово, формулируя и проверяя гипотезы, которые помогут нам сузить список возможных причин происходящего.
Формулирование гипотезы
Происходящее, на первый взгляд, выглядит как ошибка CSS. Возможно, при наведении указателя мыши на кнопку к ней применяются какие-то стили, которые и ломают макет. Возможно, во всём виноват атрибут overflow
SVG-изображения. Кроме того, возникает такое ощущение, что ошибка происходит без какой-то определённой системы при перерисовке страницы по разным поводам (событие paint
при изменении размеров окна браузера, при наведении указателя мыши на кнопку, при щелчке по ней и так далее).
Начнём с самого простого и очевидного предположения. Представим, что ошибка кроется в CSS. Мы можем исходить из предположения о том, что в браузере Safari есть ошибка, которая приводит к неправильному выводу SVG-изображений при применении к SVG-элементам каких-то специфических стилей. Например, вроде тех стилей, что используются для построения flex-макетов.
Только что мы сформулировали гипотезу. Нашим следующим шагом будет проведение испытания, которое либо подтвердит, либо опровергнет эту гипотезу. Результат каждого испытания даст нам новые сведения об ошибке и поможет в формулировании следующих гипотез.
Упрощение проблемы
Мы будем пользоваться стратегией отладки, которая называется «упрощение проблемы». Это позволит нам точно определить место возникновения ошибки. В одной лекции по информатике Корнеллского университета эту стратегию описывают как «подход по постепенному избавлению от кода, который не имеет отношения к ошибке».
Исходя из предположения о том, что ошибка кроется в CSS, мы можем, в итоге, или найти причину ошибки, или исключить из уравнения CSS, что уменьшит количество возможных причин возникновения ошибки и снизит сложность проблемы.
Давайте испытаем нашу гипотезу. Попытаемся её подтвердить. В таком случае, если временно отключить от страницы все стили, не являющиеся стандартными, это должно привести к тому, что ошибка появляться перестанет.
Вот код, с помощью которого подключается соответствующий файл стилей:
import 'css/app.css';
Я, для демонстрации вывода элементов без CSS, создал этот CodePen-проект. В React SVG-графика импортируется в проект в виде компонента, потом соответствующий код встраивается в HTML с помощью webpack.
Обычный вид кнопок
Если открыть вышеупомянутый проект в Safari и щёлкнуть по одной из крупных кнопок, то окажется, что ошибка никуда не делась. Она происходит и при загрузке страницы, но при использовании CodePen для вызова ошибки нужно щёлкнуть по кнопке.
Ошибка никуда не делась и при отключении CSS (Safari 13)
В результате мы можем сделать вывод о том, что CSS тут ни при чём. Правда, мы можем обратить внимание на то, что в таких условиях неправильно выводятся только две из пяти кнопок. Запомним это и перейдём к следующей гипотезе.
Изоляция ошибки
Наша следующая гипотеза заключается в том, что в Safari есть ошибка, возникающая при выводе SVG-изображений внутри HTML-элементов . Так как проблема возникает при выводе двух первых кнопок — изолируем первую из них и посмотрим на то, что произойдёт.
Сара Дразнер в этом материале объясняет важность изоляции. Я очень рекомендую почитать этот материал тем, кого интересуют подробности об инструментах отладки и о различных подходах к поиску ошибок. Вот цитата из этого материала: «Изоляция — это, возможно, самый главный базовый принцип отладки. Код, работающий в наших проектах, может быть разбросан по разным библиотекам и фреймворкам. В работе над проектами могут принимать участие многие люди, а некоторые из тех, кто внёс вклад в развитие проектов, больше над ними не работают. Изоляция проблемы помогает нам медленно отсекать то, что не влияет на появление ошибки. Это позволяет, в итоге, найти источник проблемы и сосредоточиться на нём».
Изоляцию ошибки часто называют «сокращённым тестовым случаем».
Я перенёс кнопку на отдельную пустую страницу (создал отдельный тест для неё). А именно, был создан этот CodePen-проект, предназначенный для исследования кнопки в изоляции. Хотя мы пришли к выводу о том, что CSS причиной проблемы не является, нам нужно оставить стили отключёнными до тех пор, пока мы не обнаружим реальную причину происходящего. Это позволит нам максимально упростить проблему.
Страница, на которой присутствует лишь кнопка
Если открыть этот проект в Safari, то окажется, что ошибку нам больше вызвать не удаётся. Даже после щелчка по кнопке изображения не меняется. Но это нельзя счесть приемлемым решением проблемы. Однако код этого CodePen-проекта даёт нам отличную базу для создания минимального воспроизводимого примера.
Минимальный воспроизводимый пример
Основное различие двух предыдущих CodePen-проектов — это комбинация кнопок. Исследование всех возможных комбинаций кнопок позволяет нам прийти к выводу о том, что проблема возникает только тогда, когда событие paint
возникает для более крупного SVG-изображения, рядом с которым на странице расположено более мелкое SVG-изображение.
Знание об этом позволило создать минимальный воспроизводимый пример, который позволил воспроизвести ошибку и при этом избавиться от каких-либо ненужных элементов. Благодаря минимальному воспроизводимому примеру мы можем более глубоко изучить проблему и выделить именно тот участок кода, который её вызывает.
Вот CodePen-проект, о котором идёт речь.
Минимальный воспроизводимый пример
Если открыть этот проект в Safari и щёлкнуть по кнопке, то мы снова столкнёмся с проблемой. Это позволит сформулировать гипотезу о том, что два SVG-элемента как-то конфликтуют друг с другом. Если наложить второе SVG-изображение на первое, то окажется, что размер того, что осталось от фона первого изображения, в точности соответствует размерам более мелкого изображения.
Рисунок, на котором второе изображение размещено поверх первого, искажённого в результате возникновения ошибки
Разделяй и властвуй
Мы смогли воспроизвести ошибку, воспользовавшись минимальным количеством элементов, представленных парой SVG-изображений. Теперь мы собираемся ещё сильнее сократить область поиска проблемы, сузив её до конкретного фрагмента SVG-кода, который является источником проблемы. Если мы понимаем принципы функционирования SVG-кода лишь в общих чертах и при этом хотим найти источник проблемы, это значит, что мы можем воспользоваться стратегией поиска по бинарному дереву, используя подход, известный как «разделяй и властвуй». Вот ещё одно извлечение из лекции по информатике Корнеллского университета: «Например, вы можете, начиная с исследования большого фрагмента кода, поместить код проверки в середине изучаемого фрагмента. Если ошибка здесь не произойдёт — это значит, что её источник находится во второй половине кода. В противном случае её источник находится в первой половине кода».
При исследовании SVG-кода можно попытаться удалить элемент
из описания первого изображения (и ещё
, так как в этом блоке, всё равно, ничего нет). Давайте поинтересуемся тем, какие именно задачи решает элемент
. Отличное объяснение этого можно найти здесь. А именно, речь идёт о следующем: «Для применения фильтров к SVG-изображениям существует специальный элемент, который называется
. Он, по сути, напоминает элементы, предназначенные для работы с линейными градиентами, с масками, шаблонами и с другими графическими эффектами. Элемент
никогда не выводится на экран самостоятельно. Он используется лишь как что-то такое, на что можно ссылаться, используя атрибут filter
в SVG-коде или функцию url()
в CSS».
В нашем SVG-изображении фильтр используется для того чтобы добавить в нижней части изображения небольшую внутреннюю тень. После удаления фильтра из кода первого изображения мы ожидаем исчезновения этой тени. Если после этого проблема не исчезнет, то мы можем сделать вывод о том, что что-то не так с другим кодом описания SVG-элемента
Я создал ещё один CodePen-проект для того чтобы продемонстрировать результаты этого испытания.
Последствия удаления элемента
Проблема, как несложно заметить, никуда не делась. А внутренняя тень продолжает выводиться даже после удаления кода фильтра. Но теперь, кроме прочего, проблема появляется во всех браузерах. Это позволяет нам сделать вывод о том, что ошибка находится где-то в оставшемся коде описания кнопки. Если удалить оставшийся id
из
, то тень исчезает. Что же тут происходит?
Взглянем ещё раз на вышеприведённое определение элемента
и обратим внимание на следующие слова: «Элемент
никогда не выводится на экран самостоятельно. Он используется лишь как что-то такое, на что можно ссылаться, используя атрибут filter
в SVG». (Фрагмент текста выделил я.)
Итак, зная это, мы можем сделать вывод о том, что объявление фильтра из второго SVG-изображения применяется к первому SVG-изображению, что и приводит к возникновению ошибки.
Исправление ошибки
Теперь мы знаем о том, что наша проблема связана с элементом
. Мы знаем и то, что оба SVG-изображения имеют такой элемент, так как фильтр используется для создания внутренней тени круглой формы. Сравним код двух SVG-изображений и подумаем о том, сможем ли мы объяснить ошибку и исправить её.
Я упростил код обоих изображений, что позволит чётко увидеть то, что в нём происходит.
Вот — код первого SVG-изображения:
Вот — код второго изображения:
Анализируя эти два фрагмента, можно заметить то, что в конструкции id=filter0_ii
используется один и тот же идентификатор. Safari применяет к элементам определение фильтра, разобранное браузером последним (в нашем случае это — фильтр второго изображения). Это и приводит к тому, что первое изображение оказывается обрезанным. Его исходный размер — 48px
, а после применения фильтра из него вырезается кусок размером 26px
. Свойство id
в DOM должно иметь уникальное значение. Если же на странице имеется несколько одинаковых id
, браузер не может разобраться в том, какое из них ему нужно использовать. А так как свойство filter
переопределяется при возникновении каждого события paint
, то, в зависимости от того, какое из определений будет готово первым (тут возникает нечто вроде состояния гонок), ошибка или появляется, или нет.
Попробуем назначить уникальные значения id
в коде каждого из изображений и посмотрим на результаты. Вот соответствующий CodePen-проект.
Назначение уникальных id решило проблему
Если теперь открыть проект в Safari и щёлкнуть по кнопке, можно будет убедиться в том, что проблему мы решили, назначив уникальный id
фильтрам, используемым в SVG-изображениях. Если задуматься над тем фактом, что в проекте были неуникальные значения для атрибута вроде id
, это позволит прийти к выводу о том, что проблема должна была появляться во всех браузерах, а не только в Safari. Но по какой-то причине другие браузеры (включая Chrome и Firefox), как кажется, обрабатывали эту необычную ситуацию без ошибок. Хотя, это может быть простым совпадением.
Итоги
Это было то ещё приключение! Мы начали, зная лишь о том, что в проекте есть некая ошибка, которая то появляется, то нет, а в итоге полностью поняли причины происходящего и справились с проблемой. Отладка кода пользовательского интерфейса и выяснение причин искажений графических элементов могут вызывать сложности в том случае, если суть происходящего неясна. Но, к нашему счастью, существуют стратегии отладки, способные помочь в поиске причин даже самых запутанных ошибок.
Сначала мы упростили проблему, сформулировав гипотезы, которые позволили нам убрать из проекта компоненты, не имеющие отношения к ошибке (стили, разметку, динамические события и прочее). После этого мы изолировали разметку и нашли минимальный воспроизводимый пример. Это позволило нам сосредоточиться на небольшом фрагменте кода. И мы, наконец, выявили проблему, воспользовавшись стратегией «разделяй и властвуй», что позволило избавиться от ошибки.
Благодарю всех, кто нашёл время на чтение этой статьи. Но, прежде чем мы закончим разговор, позволю себе рассказать вам об ещё одной стратегии отладки, упомянутой в лекциях Корнеллского университета. Речь там идёт о том, что в процессе работы нужно устраивать перерывы, отдыхать и освобождать голову от всех мыслей: «Если на отладку уходит слишком много времени, то программист устаёт. Может оказаться так, что он, пребывая в таком состоянии, трудится впустую. В подобной ситуации стоит устроить перерыв и выбросить всё из головы. А через некоторое время стоит попытаться взглянуть на проблему с другой точки зрения».
Как вы занимаетесь исправлением непонятных ошибок?