Хабрахабр

Фрактал имён элементов

Число рук равно 2. Здравствуйте, меня зовут… Человек. Группа крови равна 1. Число ног равно 2. Резус равен истине.

И будете правы. Вам может показаться, что лишь по этой информации, без имени, фамилии и даже никнейма меня сложно отличить от множества других авторов статей. И это никого не волнует. Однако, во фронтенде я часто вижу, как имя элемента заменяется его описанием.

Три мужика и девочка

Присаживайтесь поудобнее, впереди вас ждёт увлекательное путешествие по серьёзным проблемам серьёзных проектов, которые, тем не менее, зачастую недооценивают.

Вам нужно протестировать, что, например, в Яндексе в правом верхнем углу выводится правильное имя залогиненного пользователя. Представьте себя в роли писателя автоматических End-To-End тестов.

Морда Яндекса глазами Админа

Дайте заверстать эту страницу типичному верстальщику и он родит вам нечто такое:

<body> <aside> <div class="card"> <div class="username">admin</div> </div> <!-- какой-то хтмл --> </aside> <section> <!-- какой-то хтмл --> </section>
</body>

Вопрос на засыпку: как найти дом-элемент, где выводится имя пользователя?

Два пути, а суть одна

Тут у тестировщика есть выбор из двух стульев:

  1. Написать css или xpath селектор вида aside > .card > .username и молиться, чтобы в боковой панели никогда не появилось других карточек. А в карточке не появилось других имён пользователей. И чтобы никто её не поменял на какую-нибудь кнопочку. И не завернул её в какую-нибудь панельку. Короче, это очень хрупкий селектор, использование которого будет ломать тесты при малейших изменениях на странице.
  2. Попросить разработчика добавить уникальный для страницы идентификатор. Это верный путь наслать на себя гнев разработчика. Ведь у него всё на компонентах. А компоненты выводятся много где, и не знают (и не должны знать) ничего о приложении. Наивно полагать, что какой-либо компонент всегда будет на странице в единственном экземпляре, а значит в сам компонент нельзя зашивать идентификатор. Но на уровне приложения есть только использование компонента LegoSidebar. А прокидывать идентификаторы через несколько уровней вложенности компонент — та ещё оказия.

Причём, как правило перекос в сторону последних. Как видно, один вариант хуже другого — страдают либо разработчики, либо тестировщики. Потому как они вступают в дело как правило, когда первые уже закончили разработку этой фичи, и вовсю пилят другую.

Посмотрим, как справились с вёрсткой этого простого блока верстальщики из Яндекса (пришлось удалить 90% мусора, чтобы была видна суть):

<div class="desk-notif-card"> <div class="desk-notif-card__card"> <div class="desk-notif-card__domik-user usermenu-link"> <a class="home-link usermenu-link__control" href="https://passport.yandex.ru"> <span class="username desk-notif-card__user-name">admin</span> </a> <!-- какой-то хтмл --> </div> </div>
</div>
<!-- какой-то хтмл -->

Но можно ли быть уверенным, что такая карточка всегда будет одна на странице? Благодаря БЭМ, у нужного нам элемента есть в имени класса информация о контексте на 1 уровень вверх (какое-то имя пользователя в какой-то карточке). А значит, опять приходится выбирать между двумя табуретами. Смелое предположение.

Ой, какой сложный выбор

Ну он и поставил: А вот другой пример с полем поиска, где разработчика заставили поставить идентификатор.

<input class="input__control input__input" id="text" name="text"
/>

Нет, плоха архитектура, не помогающая генерировать глобально уникальные идентификаторы. Разработчик плохой?

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

База кликов по лицам

Даже если разработчики озаботились тем, чтобы уже записывались все клики по всем страницам с сохранением всей иерархии элементов в момент клика, вы всё-равно не сможете составить правильный селектор, чтобы отфильтровать нужные клики. Информация нужна вам вот прямо сейчас, поэтому скамья у вас есть только одна — работать с той статистикой, что уже собрана. Особенно, если вёрстка менялась со временем. И даже если вы попросите это сделать разработчика — он тоже скорее всего где-то ошибётся.

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

Чую здесь нужен идентификатор

В случае с Яндексом плод героических усилий разработчиков выглядит так:

<div class="desk-notif-card__domik-user usermenu-link"> <a class="home-link usermenu-link__control" href="https://passport.yandex.ru" data-statlog="notifications.mail.login.usermenu.toggle" > <!-- какой-то хтмл --> </a> <a class="home-link desk-notif-card__user-icon-link usermenu-link__control avatar" href="https://passport.yandex.ru" data-statlog="notifications.mail.login.usermenu.toggle-icon" > <!-- какой-то хтмл --> </a>
</div>

Чтобы они были так же понятны человеку. Эх, а как было бы классно, чтобы у любого элемента всегда были такие идентификаторы. Но с ручным приводом счастья не достигнуть. И при этом были стабильными, а не менялись при любом изменении вёрстки.

Вам нужно по особенному стилизовать имя пользователя в карточке в боковой панели Яндекса. Представьте себя в роли верстальщика. Вот ваш компонент: Сделаем первый подход к снаряду, без использования БЭМ.

const Morda = ()=> <div class="morda"> <LegoSidebar /> </div>

А вот пачка компонент, поддерживаемая совсем другими ребятами:

const LegoSidebar = ( { username } )=> <aside className="lego-sidebar"> <LegoCard> <LegoUsername>{ username }</LegoUsername> </LegoCard> </aside> const LegoCard = ( {} , ... children )=> <div className="lego-card"> { ... children } </div> const LegoUsername = ( {} , ... children )=> <div className="lego-username"> { ... children } </div>

Всё в сумме даёт такой результат:

<body class="morda"> <aside class="lego-sidebar"> <div class="lego-card"> <div class="lego-username">admin</div> </div> <!-- какой-то хтмл --> </aside>
</body>

Тут определённо нужна валерьянка

Стойте и ждите, пока эти другие ребята добавят в свои компоненты прокидывание кастомного класса через LegoSidebar, до LegoUsername: Если используется изоляция стилей, то присесть вам попросту не на что.

const LegoSidebar = ( { username , rootClass , cardClass , usernameClass } )=> <aside className={ "lego-sidebar " + rootClass }> <LegoCard rootClass={ cardClass }> <LegoUsername rootClass={ usernameClass}>{ username }</LegoUsername> </LegoCard> </aside> const LegoCard = ( { rootClass } , ... children )=> <div className={ "lego-card " + rootClass }> { ... children } </div> const LegoUsername = ( { rootClass } , ... children )=> <div className={ "lego-username " + rootClass }> { ... children } </div>

Иначе они помрут под грузом лапши из копипасты. И хорошо, если они вложены друг в друга непосредственно, а не через десяток промежуточных компонент.

Преодолевая трудности, созданные своими же руками

Если же изоляция не используется, то добро пожаловать в кресло из хрупких селекторов:

.morda .lego-sidebar > .lego-card > .lego-username:first-letter { color : inherit;
}

Однако, если бы у нас был инструмент, который бы брал локальные имена:

const Morda = ()=> <div> {/* какой-то контент*/} <Lego_sidebar id="sidebar" /> </div> const Lego_sidebar = ( { username } )=> <aside> <Lego_card id="profile"> <Lego_username id="username">{ username }</Lego_username> </Lego_card> </aside> const Lego_card = ( {} , ... children )=> <div> { ... children } </div> const Lego_username = ( {} , ... children )=> <div> { ... children } </div>

Склеивал бы их с учётом вложенности компонент, генерируя классы вплоть до корня приложения:

<body class="morda"> <aside class="lego_sidebar morda_sidebar"> <div class="lego_card lego_sidebar_profile morda_sidebar_profile"> <div class="lego_username lego_sidebar_username morda_sidebar_username">admin</div> </div> <!-- какой-то хтмл --> </aside>
</body>

То мы могли бы стилизовать любой элемент, как бы глубоко он ни находился:

.morda_sidebar_username:first-letter { color : inherit;
}

Такого не бывает. Не, это фантастика.

Через 5 лет 99% хипстерского кода можно только выбросить

Высокоэффективные реактивные алгоритмы с применением VirtualDOM, IncrementalDOM, DOM Batching и прочих WhateverDOM позволяют вам всего лишь за считанные секунды генерировать такого вида DOM для скрам-доски: Представьте себя в роли разработчика библиотеки рендеринга.

<div class="dashboard"> <div class="column-todo"> <div class="task"> <!-- много html --> </div> <!-- много других карточек --> </div> <div class="column-wip"> </div> <div class="column-done"> </div>
</div>

Из такого вида стейта:

{ todo : [ { /* много данных */ }, /* много задач */ ] , wip : [] , done : [] ,
}

Казалось бы, нужно просто взять DOM элемент задачи и переместить его в другое место в DOM-e. И вот незадача: пользователь начинает дрегендропить задачи туда-сюда и ожидает, что происходить это будет быстро. Короче, менять DOM вручную — это как сидеть на одноколёсном велосипеде: одно не осторожное движение и ничто вас уже не спасёт от силы тяжести. Но это придётся вручную работать с DOM и быть уверенным, что во всех местах задачи всегда рендерятся в адсолютно одно и то же DOM дерево, что зачастую не так — мелкие различия обычно есть. Надо как-то объяснять системе рендеринга, чтобы она понимала, где задача перенесена, а где одна была удалена, а другая добавлена.

Ошибка переноса идентичности не в то тело

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

Но это слишком частные решения. Пока вы переносите элементы в рамках одного родительского DOM элемента, вам могут помочь аттрибут key из Реакта, параметр ngForTrackBy из Ангуляра и аналогичные штуки в других фреймворках. Стоит перенести задачу в другую колонку, и все эти оптимизации перестают работать.

В отличие от упомянутых выше костылей к рендерингу списков, глобальные идентификаторы решают проблему системно и не сломаются, даже если в колонках появятся группироки или ещё какая дичь: Но если бы у каждого DOM элемента был был глобально уникальный идентификатор, не зависящий от того, куда этот элемент отрендерен, то использование getElementById позволило бы быстро переиспользовать существующее DOM дерево при переносе сущности из одного места в другое.

<div id="/dashboard"> <div id="/dashboard/column-todo"> <div id="/dashboard/todo/priority=critical"> <div id="/dashboard/task=y43uy4t6"> <!-- много html --> </div> <!-- много других карточек --> </div> <!-- другие группы по приоритетам --> </div> <div id="/dashboard/column-wip"> <div id="/dashboard/wip/assignee=jin"></div> <!-- другие группы по ответственному --> </div> <div id="/dashboard/column-done"> <div id="/dashboard/done/release=0.9.9"></div> <!-- другие группы по релизам --> </div> </div>

Колонки с группировками

И вам нужно добавить div вот туда. Представьте себя в роли верстальщика. Теперь убейте себя. Представили? Вы уже испорчены html-ом.

title — имя этого блока, отражающее его семантику в месте использования. На самом деле вам нужно не div добавить, а блок названия карточки. Если бы мы верстали на TypeScript, то это выражалось бы так: div — тип блока, отражающий его внешность и поведение независимо от места использования.

const Title : DIV

Мы можем тут же создать экземпляр типа:

const Title : DIV = new DIV({ children : [ taskName ] })

И позволить тайпскрипту вывести тип автоматически:

const Title = new DIV({ children : [ taskName ] })

Ну а тут уже и до HTML не далеко:

const Title = <div>{ taskName }</div>

Это первичная семантика данного элемента. Обратите внимание, что Title — это не просто случайное имя переменной, которой воспользовался и выбросил. И чтобы её не терять она должна быть отражена в результате:

const Title = <div id="title">{ taskName }</div>

И снова избавляемся от тавтологии:

<div id="title">{ taskName }</div>

Добавим остальные элементы:

<div id="task"> <div id="title">{ taskName }</div> <div id="deadline">{ taskDue }</div> <div id="description">{ taskDescription }</div>
</div>

Учтём, что кроме заголовка карточки задачи может быть много других заголовков и в том числе заголовков карточек других задач:

<div id="/dashboar/column-todo"> <div id="/dashboard/column-todo/title">To Do</div> <div id="/dashboard/task=fh5yfp6e"> <div id="/dashboard/task=fh5yfp6e/title">{ taskName }</div> <div id="/dashboard/task=fh5yfp6e/deadline">{ taskDue }</div> <div id="/dashboard/task=fh5yfp6e/description">{ taskDescription }</div> </div> <div id="/dashboard/task=fhty50or"> <div id="/dashboard/task=fhty50or/title">{ taskName }</div> <div id="/dashboard/task=fhty50or/deadline">{ taskDue }</div> <div id="/dashboard/task=fhty50or/description">{ taskDescription }</div> </div>
</div>

Таким образом у нас для каждого элемента формируются человекопонятные идентификаторы предельно точно отражающие их семантику и потому являющиеся глобально уникальными.

Хотя карточка задачи /dashboard/task=fh5yfp6e и находится в колонке /dashboard/todo, но принадлежит она дашборду /dashboard. Обратите внимание, что семантика определяется по принадлежности, а не по расположению. Он её настроил. Именно он её создал. Он ею полностью управляет. Он дал ей имя и обеспечил уникальность её идентификатора. Он же её и уничтожит.

Знай, кто твой папочка

А вот использование "правильных html тегов" — это не семантика, это типизация:

<section id="/dashboard/column-todo"> <h4 id="/dashboard/column-todo/title">To Do</h4> <figure id="/dashboard/task=fh5yfp6e"> <h5 id="/dashboard/task=fh5yfp6e/title">{ taskName }</h5> <time id="/dashboard/task=fh5yfp6e/created">{ taskCreated }</time> <time id="/dashboard/task=fh5yfp6e/deadline">{ taskDue }</time> <div id="/dashboard/task=fh5yfp6e/description">{ taskDescription }</div> </figure>
</section>

Обратите внимание на два тега time имеющих совершенно разную семантику.

У вас предельно простая задача — перевести строку текста с английского на русский. Представьте себя в роли переводчика. Если это действие, то переводить надо как "Завершить", а если состояние, то как "Завершено", но если это состояние задачи, то "Выполнено". Вам досталась строка "Done". И тут у разработчика опять есть две лавки: Без информации о контексте употребления невозможно правильно перевести текст.

  1. Снабжать тексты комментариями с информацией о контексте. А комментарии писать лень. И насколько полная нужна информация не понятно. И кода уже получается больше, чем при получении текстов по ключу.
  2. Получать тексты по ключу, прочитав который можно понять контекст употребления. Вручную заданный он не гарантирует полноту информации, но он хотя бы будет уникальным.

Опять выбор из двух зол

Тогда нам нужно в качестве ключа использовать комбинацию из имени типа (компонента, шаблона), локального имени элемента в рамках этого типа и имени его свойства. Но что если мы не хотим абы как сидеть, а хотим гордо стоять на твёрдой основе из лучших практик? А для текста "Done" на кнопке завершения задачи в карточке задачи идентификатор будет уже github_issues_task-card:button-done:label). Для текста "Done" как названия колонки таким ключом будет github_issues_dashboard:column-done:title. Если мы именуем их явно, то у нас есть возможность автоматизировать генерацию различных ключей и идентификаторов. Это, конечно, не те идентификаторы, о которых мы говорили ранее, но формируются эти ключи из тех же имён, которые мы явно или неявно даём составным элементам. Но если неявно, то приходится задавать эти ключи и идентификаторы вручную и надеяться, что не вспыхнет бардака с именованием одной и той же сущности в разных местах по разному.

Прилетает вам баг-репорт: Представьте себя в роли прикладного разработчика.

Ничего не работает! Всё пропало! Приложение не открывается, в консоли такое: Uncaught TypeError: f is not a function at <Button> at <Panel> at <App>

— Ага, та самая известная f из какой-то кнопки, на какой-то панели, — заявил бы диванный эксперт, — Всё ясно.

Тут и дураку понятно

А если бы это был всего один уникальный идентификатор: Или нет?

/morda/sidebar/close

ВасяП, это к тебе, шевели булками. — Ага, кнопка закрытия сайдбара сломалась.

Садится Вася за приложуху, вбивает полученный идентификатор в девелоперской консоли, и тут же получает экземпляр компонента, где сразу видно, что кнопке в качестве обработчика нажатия кто-то умный передал строку:

Кажется это не совсем HTML

И по этому идентификатору этот компонент легко получить, без плясок вокруг дебаггера. Хорошо, что у каждого компонента есть идентификатор. А что если сам идентификатор будет программным кодом, для получения компонента? Правда нужен инструмент, позволяющий найти компонент по идентификатору.

<button id="Components['/morda/sidebar/close']">X</button>

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

Если вы используете $mol, то вам ничего делать не надо — просто садитесь и получаете вибромассаж по полной:

$ya_morda $mol_view sub / <= Sidebar $ya_lego_sidebar $ya_lego_sidebar $mol_view sub / <= Profile $ya_lego_card sub / <= Username $ya_lego_username sub / <= username \ $ya_lego_card $mol_view $ya_lego_username $mol_view

Из этого описания компонент генерируется следующий DOM: Программист просто синтаксически не сможет не дать компоненту уникальное имя.

<body id="$ya_morda.Root(0)" ya_morda mol_view > <ya_lego_sidebar id="$ya_morda.Root(0).Sidebar()" ya_lego_sidebar mol_view ya_morda_sidebar > <ya_lego_card id="$ya_morda.Root(0).Sidebar().Profile()" ya_lego_card mol_view ya_lego_sidebar_profile ya_morda_sidebar_profile > <ya_lego_username id="$ya_morda.Root(0).Sidebar().Username()" ya_lego_username mol_view ya_lego_sidebar_username ya_morda_sidebar_username > admin </ya_lego_username> </ya_lego_card> </ya_lego_sidebar> </body>

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

Uncaught (in promise) Error: Test error at $mol_state_local.value("mol-todos-85").calculate at $mol_state_local.value("mol-todos-85").pull at $mol_state_local.value("mol-todos-85").update at $mol_state_local.value("mol-todos-85").get at $mol_app_todomvc.Root(0).task at $mol_app_todomvc.Root(0).task_title at $mol_app_todomvc.Root(0).task_title(85).calculate at $mol_app_todomvc.Root(0).task_title(85).pull at $mol_app_todomvc.Root(0).task_title(85).update at $mol_app_todomvc.Root(0).task_title(85).get at $mol_app_todomvc.Root(0).Task_row(85).title at $mol_app_todomvc.Root(0).Task_row(85).Title().value at $mol_app_todomvc.Root(0).Task_row(85).Title().event_change

Побеждают не те, кто хочет сделать мир лучше

Можете сделать себе такой же да ещё и с генерацией классов для стилизации. Если вы подсели на Реакт, то можете пересесть на кастомный JSX трансформер для генерации глобально уникальных идентификаторов по локальным именам вложенных в компонент элементов. На примере с дашбордом шаблоны выглядят примерно так:

const Dashboard = ()=> ( <div> <Column id="/column-todo" title="To Do"> <Task id="/task=fh5yfp6e" title="foobar" deadline="yesterday" content="Do it fast!" /> </Column> <Column id="/column-wip" title="WIP" /> <Column id="/column-done" title="Done" /> </div>
) const Column = ( { title } , ... tasks )=> ( <div> <div id="/title">{ title }</div> { tasks } </div>
)
const Task = ({ title , deadline , description })=> ( <div> <div id="/title">{ title }</div> <div id="/deadline">{ deadline }</div> <div id="/description">{ description }</div> </div>
) const App = ()=> <Dashboard id="/dashboard" />

На выходе генерируя:

<div id="/dashboar"> <div id="/dashboar/column-todo"> <div id="/dashboard/column-todo/title">To Do</div> <div id="/dashboard/task=fh5yfp6e"> <div id="/dashboard/task=fh5yfp6e/title">foobar</div> <div id="/dashboard/task=fh5yfp6e/deadline">yesterday</div> <div id="/dashboard/task=fh5yfp6e/description">Do it fast!</div> </div> </div> <div id="/dashboar/wip"> <div id="/dashboard/column-wip/title">WIP</div> </div> <div id="/dashboar/done"> <div id="/dashboard/column-done/title">Done</div> </div>
</div>

Ох, сколько копипасты

Или хотя бы на добавление API, через который такие возможности можно было бы реализовать самостоятельно. Если же вы в заложниках у какого-либо другого фреймворка, то создавайте его авторам issue на добавление опциональной возможности генерации идентификаторов и классов.

Резюмируя, напомню, зачем надо давать уникальные имена всем элементам:

  • Простота и стабильность E2E тестов.
  • Простота сбора статистики использования приложения и её анализа.
  • Простота стилизации.
  • Эффективность рендеринга.
  • Точная и исчерпывающая семантика.
  • Простота локализации.
  • Удобство отладки.
Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть