Используем DOM как Pro

Spread the love

Как перестать бояться DOM, использовать его в полной мере и действительно начать любить его

Перевод статьи Danny MoerkerkeUsing the DOM like a Pro

Когда я впервые начал работать в качестве профессионального веб-разработчика в 2008 году, я знал немного HTML, CSS и PHP. В то же время я также изучал такую штуку, как JavaScript, потому что она позволяла мне показывать и скрывать элементы и делать классные вещи, такие как выпадающие меню.

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

После некоторых поисков я нашел причудливое решение на основе Flash и библиотеки JavaScript под названием MooTools. В MooTools есть такая классная функцию $ для выбора элементов DOM и она поставляется с модулями, такими как индикаторы выполнения и запросы Ajax. Несколько недель спустя я открыл для себя jQuery и был поражен.

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

Перенесемся в 2019 году, и теперь мир управляться фреймворками. Если вы начали изучать веб разработку в последнее десятилетие, есть вероятность, что вы вряд ли когда-либо столкнулись с «нативным» DOM. Возможно, вам даже это никогда не было нужно.

Несмотря на то, что такие фреймворки, как Angular и React, привели к значительному снижению популярности jQuery, он по-прежнему используется ошеломляющими 66 миллионами веб-сайтов, что, по оценкам, составляет около 74% всех сайтов в мире.

Наследие jQuery впечатляет, и отличным примером того, как он повлиял на стандарты, являются методы querySelector и querySelectorAll, которые имитируют функцию $ в jQuery.

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

Но нативный DOM API многословен.

Я имею в виду, использование $ вместо document.querySelectorAll.

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

Давайте начнем!

Выбор элементов

Выбор одного элемента

Чтобы выбрать один элемент, используется любой допустимый селектор CSS. Так например:

document.querySelector(/* your selector */)

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

document.querySelector('.foo')            // class selector
document.querySelector('#foo')            // id selector
document.querySelector('div')             // tag selector
document.querySelector('[name="foo"]')    // attribute selector
document.querySelector('div + p > span')  // you go girl!

Если нет соответствующих элементов, он вернет null.

Выбор нескольких элементов

Для выбора нескольких элементов используйте:

document.querySelectorAll('p')  // selects all <p> elements

Вы можете использовать document.querySelectorAll так же, как document.querySelector. Подойдет любой действительный селектор CSS, и единственное отличие состоит в том, что querySelector вернет один элемент, тогда как querySelectorAll вернет статический NodeList, содержащий найденные элементы. Если элементы не найдены, он вернет пустой NodeList.

NodeList – это итеративный объект, который похож на массив, но на самом деле это не массив, поэтому у него нет тех же методов. Вы можете запустить forEach на нем, но не map, reduce или find.

Если вам нужно запустить методы массива, вы можете просто превратить его в массив, используя […x] или Array.from:

const arr = [...document.querySelectorAll('p')];

или

const arr = Array.from(document.querySelectorAll('p'));
arr.find(element => {...});  // .find() now works

Метод querySelectorAll отличается от методов, таких как getElementsByTagName и getElementsByClassName, тем, что эти методы возвращают HTMLCollection, являющуюся живой коллекцией, тогда как querySelectorAll возвращает NodeList, который является статическим.

Поэтому, если вы сделаете getElementsByTagName(‘p’) и один <p> позже будет удален из документа, он также будет удален из возвращенного HTMLCollection.

Но если бы вы сделали querySelectorAll (‘p’) и один <p> позже будет удален из документа, он все равно будет присутствовать в возвращенном NodeList.

Другое важное отличие состоит в том, что HTMLCollection может содержать только HTMLElements, а NodeList может содержать любой тип Node.

Относительный поиск

Вам не обязательно запускать querySelector(All) для всего документа. Вы можете запустить его на любом элементе HTMLElement для запуска относительного поиска:

const div = document.querySelector('#container');
div.querySelectorAll('p')  // finds all <p> tags in #container only

Но это все еще многословно!

Если вы все еще беспокоитесь о дополнительных нажатиях клавиш, вы можете использовать оба метода:

const $ = document.querySelector.bind(document);
$('#container');

const $$ = document.querySelectorAll.bind(document);
$$('p');

Поднимаясь по дереву DOM

Использование CSS-селекторов для выбора элементов DOM означает, что мы можем перемещаться только по дереву DOM. Нет CSS-селекторов для перемещения вверх по дереву, чтобы выбрать родителей.

Но мы можем перемещаться по дереву DOM с помощью метода closest(), который также принимает любой допустимый селектор CSS:

document.querySelector('p').closest('div');

Это найдет ближайший родительский элемент абзаца, выбранный document.querySelector (‘p’). Вы можете связать эти вызовы, чтобы идти дальше вверх по дереву:

document.querySelector('p').closest('div').closest('.content');

Добавление элементов

Код для добавления одного или нескольких элементов в дерево DOM печально известен быстрым ростом количеством требуемых строк. Допустим, вы хотите добавить следующую ссылку на свою страницу:

<a href="/home" class="active">Home</a>

Вам нужно будет сделать:

const link = document.createElement('a');
link.setAttribute('href', '/home');
link.className = 'active';
link.textContent = 'Home';document.body.appendChild(link);

Теперь представьте, что нужно сделать это для 10 элементов …

По крайней мере, jQuery позволяет вам сделать это намного быстрее:

$('body').append('<a href="/home" class="active">Home</a>');

Ну догадайся что? Есть нативный эквивалент:

document.body.insertAdjacentHTML('beforeend', 
'<a href="/home" class="active">Home</a>');

Метод insertAdjacentHTML позволяет вставить произвольную допустимую строку HTML в DOM в четырех позициях, указанных первым параметром:

  • 'beforebegin': перед элементом
  • 'afterbegin': внутри элемента до его первого потомка
  • 'beforeend': внутри элемента после его последнего потомка
  • 'afterend': после элемента
<!-- beforebegin -->
<p>
  <!-- afterbegin -->
  foo
  <!-- beforeend -->
</p>
<!-- afterend -->

Это также значительно упрощает указание точной точки, где новый элемент должен быть вставлен. Скажем, вы хотите вставить <a> прямо перед этим <p>. Без insertAdjacentHTML вы бы сделали это:

const link = document.createElement('a');
const p = document.querySelector('p');
p.parentNode.insertBefore(link, p);

Теперь вы можете просто сделать:

const p = document.querySelector('p');
p.insertAdjacentHTML('beforebegin', '<a></a>');

Существует также эквивалентный метод для вставки элементов DOM:

const link = document.createElement('a');
const p = document.querySelector('p');
p.insertAdjacentElement('beforebegin', link);

и текста:

p.insertAdjacentText('afterbegin', 'foo');

Перемещение элементов

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

Если у вас есть этот HTML:

<div class="first">
  <h1>Title</h1>
</div><div class="second">
  <h2>Subtitle</h2>
</div>

и допустим нем необходимо <h2> вставить после <h1>:

const h1 = document.querySelector('h1');
const h2 = document.querySelector('h2');
h1.insertAdjacentElement('afterend', h2);

он будет просто перемещен, а не скопирован:

<div class="first">
  <h1>Title</h1>
  <h2>Subtitle</h2>
</div><div class="second">
  
</div>

Замена элементов

Элемент DOM может быть заменен любым другим элементом DOM с помощью метода replaceWith:

someElement.replaceWith(otherElement);

Элемент, которым он заменен, может быть новым элементом, созданным с помощью document.createElement, или элементом, который уже является частью того же документа (в этом случае он снова будет перемещен, а не скопирован):

<div class="first">
  <h1>Title</h1>
</div>
<div class="second">
  <h2>Subtitle</h2>
</div>

const h1 = document.querySelector('h1');
const h2 = document.querySelector('h2');

h1.replaceWith(h2);

// result:
<div class="first">
  <h2>Subtitle</h2>
</div>
<div class="second">
  
</div>

Удаление элементов

Просто вызовите метод remove:

const container = document.querySelector('#container');
container.remove();  // hasta la vista, baby

Гораздо лучше, чем по старинке

const container = document.querySelector('#container');
container.parentNode.removeChild(container);

Создать элемент из чистого HTML

Метод insertAdjacentHTML позволяет нам вставлять необработанный HTML в документ, но что, если мы хотим создать элемент из необработанного HTML и использовать его позже?

Для этого мы можем использовать объект DomParser и его метод parseFromString. DomParser предоставляет возможность разбора исходного кода HTML или XML в документ DOM.

Используем метод parseFromString для создания документа только с одним элементом и возвратом только этого элемента:

const createElement = domString => new DOMParser().parseFromString(domString, 'text/html').body.firstChild;

const a = createElement('<a href="/home" class="active">Home</a>');

Проверка DOM

Стандартный DOM API также предоставляет несколько удобных методов для проверки DOM. Например, matches определяет, будет ли элемент соответствовать определенному селектору:

<p class="foo">Hello world</p>

const p = document.querySelector('p');

p.matches('p');     // true
p.matches('.foo');  // true
p.matches('.bar');  // false, does not have class "bar"

Вы также можете проверить, является ли элемент дочерним по отношению к другому элементу с помощью метода contains:

<div class="container">
  <h1 class="title">Foo</h1>
</div>

<h2 class="subtitle">Bar</h2>

const container = document.querySelector('.container');
const h1 = document.querySelector('h1');
const h2 = document.querySelector('h2');

container.contains(h1);  // true
container.contains(h2);  // false

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

Вот пример с теми же элементами из предыдущего примера:

<div class="container">
  <h1 class="title">Foo</h1>
</div>

<h2 class="subtitle">Bar</h2>

const container = document.querySelector('.container');
const h1 = document.querySelector('h1');
const h2 = document.querySelector('h2');

//  20: h1 содержится в контейнере и следует за контейнером
container.compareDocumentPosition(h1); 

// 10: контейнер содержит h1 и предшествует ему
h1.compareDocumentPosition(container);

// 4: h2 следует за h1
h1.compareDocumentPosition(h2);

// 2: h1 предшествует h2
h2.compareDocumentPosition(h1);

Значение, возвращаемое из compareDocumentPosition, представляет собой целое число, биты которого представляют отношение между узлами относительно аргумента, данного этому методу.

Итак, учитывая синтаксис node.compareDocumentPostion (otherNode), возвращаемое значение будет:

  • 1: узлы не являются частью одного и того же документа
  • 2: otherNode предшествует node
  • 4: otherNode следует за node
  • 8: otherNode содержит node
  • 16: otherNode содержится в node

Может быть установлено более одного бита, поэтому в приведенном выше примере container.compareDocumenPosition (h1) возвращает 20, где можно ожидать 16, поскольку h1 содержится в container. Но h1 также следует за container (4), поэтому полученное значение равно 16 + 4 = 20.

Подробнее, пожалуйста!

Вы можете наблюдать изменения в любом узле DOM через интерфейс MutationObserver. Он включает в себя изменения текста, добавление или удаление узлов из наблюдаемого узла или изменение атрибутов узла.

MutationObserver – это невероятно мощный API для наблюдения практически за любыми изменениями, которые происходят в элементе DOM и его дочерних узлах.

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

const observer = new MutationObserver(callback);

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

const target = document.querySelector('#container');
const observer = new MutationObserver(callback);

observer.observe(target, options);

Наблюдение за целью не начинается до тех пор, пока не будет вызвано observe.

Объект options принимает следующие ключи:

  • attributes: если установлено значение true, изменения атрибутов узла будут отслеживаться
  • attributeFilter: массив имен атрибутов для просмотра, когда attributes имеют значение true, и если этот атрибут не установлен, будут отслеживаться изменения всех атрибутов узла
  • attributeOldValue: при значении true предыдущее значение атрибута будет записываться при каждом изменении
  • characterData: если установлено значение true, будет записывать изменения в тексте текстового узла, так как это работает только для текстовых узлов, а не для HTMLElements. Чтобы это работало, наблюдаемый узел должен быть node Text или, если наблюдатель отслеживает HTMLElement, опция subtree должна быть установлено в значение true, чтобы также отслеживать изменения в дочерних узлах.
  • characterDataOldValue: при значении true предыдущее значение символьных данных будет записываться при каждом изменении
  • subtree: установите в значение true, чтобы также наблюдать изменения в дочерних узлах наблюдаемого элемента.
  • childList: установите значение true, чтобы отслеживать элемент для добавления и удаления дочерних узлов. Когда для subtree установлено значение true, дочерние элементы также будут отслеживаться на предмет добавления и удаления дочерних узлов.

Когда наблюдение за элементом началось с вызова observe, обратный вызов, переданный конструктору MutationObserver, вызывается с массивом объектов MutationRecord, описывающих произошедшие изменения, и наблюдателя, который был вызван в качестве второго параметра.

MutationRecord содержит следующие свойства:

  • type: тип изменения: attributes, characterData или childList.
  • target: измененный элемент: его атрибуты, символьные данные или дочерние элементы
  • addedNodes: список добавленных узлов или пустой NodeList, если ни один из них не был добавлен
  • removedNodes: список удаленных узлов или пустой NodeList, если ни один из них не был удален
  • attributeName: имя измененного атрибута или null, если атрибут не был изменен
  • previousSibling: предыдущий соседний элемент добавленных или удаленных узлов или null
  • nextSibling: следующий соседний элемент добавленных или удаленных узлов или null

Допустим, мы хотим наблюдать изменения в атрибутах и дочерних узлах:

const target = document.querySelector('#container');
const callback = (mutations, observer) => {
  mutations.forEach(mutation => {
    switch (mutation.type) {
      case 'attributes':
        // the name of the changed attribute is in
        // mutation.attributeName
        // and its old value is in mutation.oldValue
        // the current value can be retrieved with 
        // target.getAttribute(mutation.attributeName)
        break;
      case 'childList':
        // any added nodes are in mutation.addedNodes
        // any removed nodes are in mutation.removedNodes
        break;
    }
  });
};

const observer = new MutationObserver(callback);
observer.observe(target, {
  attributes: true,
  attributeFilter: ['foo'], // only observe attribute 'foo'
  attributeOldValue: true,
  childList: true
});

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

const mutations = observer.takeRecords();
callback(mutations);
observer.disconnect();

Не бойтесь DOM

DOM API – это невероятно мощное и универсальное, хотя и многословное API. Помните, что оно предназначен для того, чтобы предоставлять разработчикам низкоуровневые строительные блоки для построения абстракций, поэтому в этом смысле оно должен быть многословным, чтобы обеспечить однозначный и понятный API.

Дополнительные нажатия клавиш не должны пугать вас от использования его в полной мере.

DOM является необходимым знанием для каждого разработчика JavaScript, поскольку вы, вероятно, используете его каждый день. Не бойтесь этого и используйте его в полной мере.


Spread the love

Используем DOM как Pro: 2 комментария

  • 25.09.2019 в 08:49
    Permalink

    > document.querySelector(‘p’).closest(‘div’);

    А теперь real life пример:

    > document.querySelector(‘.my-awesome-class’).closest(‘.my-another-class’).closes(‘div’);

    Если кодин из классов не существует на данный момент в DOM, то `Cannot read property ‘closest’ of null`
    В jQuery вернулась бы обёртка с длиной === 0

    Ответ
    • 25.09.2019 в 11:27
      Permalink

      Ничто не мешает написать свое функцию обертку с дополнительными проверками. closest все же низкоуровневое API.
      В статье ни говориться что ни нужно пользоваться jQuery, в ней говориться что не плохо бы знать низкоуровневое API и при возможности его использовать (когда это возможно), а не тащить jQuery во всех возможных ситуациях.

      Ответ

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

Ваш e-mail не будет опубликован.