DOM-Scope: создание искусственных областей видимости и управление идентификаторами элементов

от автора

Обычное DOM-дерево

Обычное DOM-дерево

В процессе работы с HTML-документами часто возникает необходимость получить доступ к конкретным элементам DOM. Обычно это делается с помощью уникальных идентификаторов, которые присваиваются нужным элементам. Однако существует вероятность, что в большом документе может оказаться несколько элементов с одинаковыми идентификаторами, что может привести к путанице.

Чтобы избежать этой проблемы, я разработал библиотеку dom-scope, которая позволяет создавать искусственные области видимости (скоупы) внутри DOM. Я хочу поделиться своим решением с сообществом Хабра.

Давайте рассмотрим пример простейшей верстки, который демонстрирует проблему дублирования имен элементов:

<span class="a">1</span> <span class="b">1</span>  <div>     <span class="a">2</span>     <span class="b">2</span>     <span class="c">2</span> </div>  <span class="c">1</span>

Наша цель — найти и выделить узлы, содержащие текст «1». Это лишь условное обозначение того, что нам необходимо найти.

В данной верстке можно выделить несколько проблем:

  1. Идентификаторы элементов повторяются, и это следует учитывать.

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

  3. Для элементов, помеченных цифрой «2», явно отсутствуют какие-либо границы области, где описываются локальные идентификаторы.

Решение с помощью DOM-Scope

Для начала устанавливаем библиотеку:

npm install dom-scope

Давайте рассмотрим код и разберём его.

import { createFromHTML, selectRefs } from "dom-scope";  // Для наглядности создадим DocumentFragment  const root = createFromHTML(/*html*/`     <span ref="a">1</span>     <span ref="b">1</span>      <div scope-ref="another_scope">         <span ref="a">2</span>         <span ref="b">2</span>         <span ref="c">2</span>     </div>      <span ref="c">1</span> `);  // неправильная аннотация HTML-элементов const wrong_annotation = {     "a": HTMLElement,     "b": HTMLDivElement,     "c": HTMLSpanElement };  try {     // выбираем элементы и проверяем соответствуют ли они аннотации     let refs = selectRefs(root, wrong_annotation); } catch (e) {     console.log(e.message);     // возникает ошибка: Элемент "b" должен быть экземпляром HTMLDivElement    // (по факту является экземпляром HTMLSpanElement) }  // правильная аннотация const annotation = {     "a": HTMLSpanElement,     "b": HTMLSpanElement,     "c": HTMLSpanElement };  let refs = selectRefs(root, annotation); const { a, b, c } = refs;  console.log(a.textContent, b.textContent, c.textContent); // outputs: 1 1 1  // добавляем наш фрагмент к документу. document.body.appendChild(root);

В этом фрагменте кода используется функция createFromHTML для создания элемента DOM из строки HTML.

Затем, с помощью selectRefs, мы пытаемся выбрать ссылки на элементы внутри созданного DOM. Параметр root может быть как существующим элементом, корнем ShadowRoot или DocumentFragment.

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

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

Важно отметить, что selectRefs не только проверяет наличие и правильность типов элементов в рантайме, но и возвращает типизированный объект, содержащий ссылки на HTML-элементы. Это особенно полезно для статического анализа типов с помощью TypeScript (и, разумеется, работает автокомплит для рефов).

Элемент с атрибутом scope-ref определяет новый скоуп в DOM. Эта область изолирует локальные идентификаторы от родительского либо дочернего скоупа . Как мы видим, скоупы могут быть вложенными, что позволяет создавать иерархическую структуру уникальных идентификаторов .

Таким образом, ссылка на идентификатор «с» будет указывать на элемент с текстом «1», исключая элементы с текстом «2».

Хотелось бы отметить, что механизм перебора элементов в скопе использует сверхбыстрый низкоуровневый API TreeWalker (https://developer.mozilla.org/en-US/docs/Web/API/TreeWalker). Как правило, этот API работает значительно быстрее, чем метод document.querySelector. В интернете можно найти множество бенчмарков, которые сравнивают производительность различных способов поиска элементов, например, этот

Продвинутое использование DOM-Scope

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

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

Пример использования DomScope:

<body>     <span ref="a">a</span>     <span ref="b">b</span>      <div scope-ref="my-scope-1">         <div ref="a">a</div>         <div ref="b">b</div>                <div ref="c">c</div>     </div> </body>
import { DomScope } from "dom-scope";  const domScope = new DomScope(document.body); const refs = domScope.refs; console.log(refs.a instanceof HTMLSpanElement); // true console.log(refs.b instanceof HTMLSpanElement); // true  // получаем ссылку на дочерний скоуп  let scope = domScope.scopes["my-scope-1"]; const {a, b, c} = scope.refs; console.log(a instanceof HTMLDivElement); // true console.log(b instanceof HTMLDivElement); // true console.log(c instanceof HTMLDivElement); // true

Я думаю, что тут все довольно просто и понятно. Поэтому идем дальше.

Инстанс domScope содержит объект настройки config со следующими полями, которые могут быть вам полезны для более глубокой настройки:

  1. ref_attr_name — задает кастомное имя атрибута, содержащее локальный идентификатор элемента;

  2. is_scope_element — функция с логикой определения является ли элемент границей скоупа (пример будет ниже);

  3. window: — ссылка на кастомный объект window, помогает тестировать, а также позволяет использовать DomScope на сервере.

    Пример бизнес-логики для определения скоупа:

<body>     <span ref="a">a</span>     <span ref="b">b</span>      <div custom-scope-attribute="custom_scope_name">         <span ref="c">c</span>     </div> </body>
import { DomScope } from "dom-scope";  const domScope = new DomScope(document.body);  domScope.config.is_scope_element = function (element) {     if (element.hasAttribute("custom-scope-attribute")) {         // проверяем именнованный это скоуп или безымянный         return element.getAttribute("custom-scope-attribute") || "";     }      return false; }  const {a, b} = domScope.refs;  let scope = domScope.scopes["custom_scope_name"]; const {c} = scope.refs;

DomScope имеет другие полезные методы для работы с элементами. Кому будет интересно, можете ознакомиться с библиотекой более подробно: https://www.npmjs.com/package/dom-scope, https://github.com/supercat1337/dom-scope.

Заключительная часть

В заключение хочу отметить, что библиотека доступна как ESM-модуль, тришейкается, она быстрая, легкая и простая в использовании.

DOM-Scope — это мощный инструмент, который значительно упрощает работу с идентификаторами в DOM. Он решает проблему дублирования идентификаторов и позволяет создавать изолированные области видимости для элементов. Это особенно полезно в сложных проектах с большим количеством элементов и вложенных структур.

С помощью DOM-Scope можно избежать путаницы с идентификаторами и значительно упростить поиск нужных элементов в DOM. Кроме того, благодаря использованию низкоуровневого API TreeWalker, библиотека обеспечивает более высокую производительность.

Я надеюсь, что моя библиотека DOM-Scope окажется полезной для сообщества Хабра. Буду рад обсудить любые вопросы и предложения в комментариях или на GitHub.


ссылка на оригинал статьи https://habr.com/ru/articles/892326/


Комментарии

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

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