Использование DOM как профессионал
фото Панкадж Патель на Скрыть
Когда я впервые начал работать профессиональным веб-разработчиком в 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
.
Если вам нужно запустить на нем методы массива, вы можете просто превратить его в массив, используя деструктурирование или Array.from
:
const arr = [...document.querySelectorAll('p')];
// or
const arr = Array.from(document.querySelectorAll('p'));
arr.find(element => {...}); // .find() now works
Итак, если бы вы сделали 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');
Это найдет ближайшего родителя <div>
элемент абзаца, выбранный document.querySelector('p')
. Вы можете связать эти вызовы, чтобы подняться выше по дереву:
document.querySelector('p').closest('div').closest('.content');
Добавление элементов
Код для добавления одного или нескольких элементов в дерево DOM печально известен тем, что быстро становится многословным. Допустим, вы хотите добавить на свою страницу следующую ссылку:
<a href="/home">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">Home</a>');
Ну угадайте что? Есть нативный эквивалент:
document.body.insertAdjacentHTML('beforeend', '<a href="/home">Home</a>');
insertAdjacentHTML
Метод позволяет вставить произвольную допустимую строку HTML в DOM в четырех позициях, указанных первым параметром:
'beforebegin': before the element
'afterbegin': inside the element before its first child
'beforeend': inside the element after its last child
'afterend': after the element
<!-- 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>
<h1>Title</h1>
</div><div>
<h2>Subtitle</h2>
</div>
и <h2>
вставляется после <h1>
:
const h1 = document.querySelector('h1');
const h2 = document.querySelector('h2');h1.insertAdjacentElement('afterend', h2);
он будет просто перемещен, а не скопирован:
<div>
<h1>Title</h1>
<h2>Subtitle</h2>
</div><div>
</div>
Замена элементов
Элемент DOM можно заменить любым другим элементом DOM, используя его replaceWith
метод:
someElement.replaceWith(otherElement);
Элемент, которым он заменяется, может быть новым элементом, созданным с помощью document.createElement
или элемент, который уже является частью того же документа (в этом случае он снова будет перемещен, а не скопирован):
<div>
<h1>Title</h1>
</div><div>
<h2>Subtitle</h2>
</div>const h1 = document.querySelector('h1');
const h2 = document.querySelector('h2');h1.replaceWith(h2);// result:<div>
<h2>Subtitle</h2>
</div><div>
</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">Home</a>');
Проверка DOM
Стандартный API DOM также предоставляет несколько удобных методов для проверки DOM. Например, matches
определяет, будет ли элемент соответствовать определенному селектору:
<p>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>
<h1>Foo</h1>
</div>
<h2>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>
<h1>Foo</h1>
</div>
<h2>Bar</h2>
const container = document.querySelector('.container');
const h1 = document.querySelector('h1');
const h2 = document.querySelector('h2');// 20: h1 is contained by container and follows container
container.compareDocumentPosition(h1); // 10: container contains h1 and precedes it
h1.compareDocumentPosition(container);// 4: h2 follows h1
h1.compareDocumentPosition(h2);// 2: h1 precedes h2
h2.compareDocumentPosition(h1);
Значение, возвращенное из compareDocumentPosition
— целое число, биты которого представляют отношение между узлами относительно аргумента, переданного этому методу.
Итак, учитывая синтаксис node.compareDocumentPostion(otherNode)
значение возвращаемого значения:
1: узлы не являются частью одного и того же документа
2: otherNode предшествует узлу
4: другой узел следует за узлом
8: otherNode содержит узел
16: otherNode содержится в узле
Может быть установлено более одного бита, поэтому в приведенном выше примере container.compareDocumenPosition(h1)
возвращает 20, где вы могли бы ожидать 16, так как h1 содержится в контейнере. Но h1 также следует за контейнером (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
называется.
Этот объект опций принимает следующие ключи:
attributes
: при значении true будут отслеживаться изменения атрибутов узла.attributeFilter
: массив имен атрибутов для наблюдения, когда атрибуты имеют значение true и это не установлено, будут отслеживаться изменения всех атрибутов узла.attributeOldValue
: при значении true предыдущее значение атрибута будет записываться всякий раз, когда происходит изменениеcharacterData
: если установлено значение true, это будет записывать изменения в тексте текстового узла, поэтому это работает только с текстовыми узлами, а не с элементами HTML. Чтобы это работало, наблюдаемый узел должен быть текстовым узлом или, если наблюдатель отслеживает HTMLElement, для поддерева параметров необходимо установить значение true, чтобы также отслеживать изменения в дочерних узлах.characterDataOldValue
: если установлено значение true, предыдущее значение символьных данных будет записываться всякий раз, когда происходит изменениеsubtree
: установите значение true, чтобы также отслеживать изменения в дочерних узлах наблюдаемого элемента.childList
: установите значение true, чтобы отслеживать элемент для добавления и удаления дочерних узлов. Когда для поддерева установлено значение true, дочерние элементы также будут отслеживаться для добавления и удаления дочерних узлов.
Когда наблюдение за элементом началось с вызова observe
обратный вызов, который был передан MutationObserver
конструктор вызывается с массивом MutationRecord
объекты, описывающие произошедшие изменения, и наблюдатель, который был вызван в качестве второго параметра.
А MutationRecord
содержит следующие свойства:
type: тип изменения, атрибуты, characterData или childList.
цель: элемент, который изменился, его атрибуты, символьные данные или дочерние элементы
addNodes: список добавленных узлов или пустой NodeList, если ни один не был добавлен.
removeNodes: список удаленных узлов или пустой NodeList, если ни один не был удален.
attributeName: имя измененного атрибута или ноль, если атрибут не был изменен.
previousSibling: предыдущий одноуровневый узел добавленных или удаленных узлов или ноль
nextSibling: следующий родственный узел добавленных или удаленных узлов или ноль
Итак, допустим, мы хотим наблюдать за изменениями атрибутов и дочерних узлов:
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 API — невероятно мощный и универсальный, хотя и многословный API. Имейте в виду, что он предназначен для предоставления разработчикам низкоуровневых строительных блоков для построения абстракций, поэтому в этом смысле он должен быть подробным, чтобы обеспечить однозначный и понятный API.
Дополнительные нажатия клавиш не должны отпугнуть вас от использования его полного потенциала.
DOM является важным знанием для каждого разработчика JavaScript, поскольку вы, вероятно, используете его каждый день. Не бойтесь этого и используйте его в полной мере.
Вы будете лучшим разработчиком для этого.
Первоначально опубликовано на Medium.com