Дете Эльза ( [email protected] ), старший технический архитектор, Blast Radius
Объектная модель документа (DOM) — один из наиболее часто используемых инструментов для управления данными XML и HTML, однако его потенциал редко используется полностью. Воспользовавшись преимуществами DOM и упростив его использование, вы получаете мощный инструмент для XML-приложений, включая динамические веб-приложения.
В этом выпуске участвует приглашенный обозреватель, друг и коллега Дете Эльза. У Дете большой опыт разработки веб-приложений с использованием XML, и я хотел бы поблагодарить его за помощь в ознакомлении с XML-программированием с использованием DOM и ECMAScript. Следите за этой колонкой, чтобы увидеть больше колонок Дете.
— Дэвид Мерц
DOM — один из стандартных API для обработки XML и HTML. Его часто критикуют за большой объем памяти, медленный и многословный подход. Тем не менее, это лучший выбор для многих приложений, и он определенно проще, чем SAX, другой основной API XML. DOM постепенно появляется в таких инструментах, как веб-браузеры, SVG-браузеры, OpenOffice и т. д.
DOM великолепен, потому что это стандарт, он широко реализован и встроен в другие стандарты. По стандарту его обработка данных не зависит от языка программирования (что может быть, а может и не быть преимуществом, но, по крайней мере, это делает способ обработки данных согласованным). DOM теперь не только встроен в веб-браузеры, но также является частью многих спецификаций на основе XML. Теперь, когда это часть вашего арсенала и, возможно, вы все еще используете его время от времени, я думаю, пришло время в полной мере воспользоваться тем, что оно дает.
Поработав некоторое время с DOM, вы увидите, как развиваются шаблоны — вещи, которые вы захотите делать снова и снова. Ярлыки помогают работать с длинными DOM и создавать понятный и элегантный код. Вот коллекция советов и приемов, которые я часто использую, а также несколько примеров JavaScript.
Первый трюк
с InsertAfter и prependChild
заключается в том, что здесь «никакого трюка».В DOM есть два метода для добавления дочернего узла в узел контейнера (обычно элемент, документ или фрагмент документа): addChild(node) и InsertBefore(node, referenceNode). Кажется, чего-то не хватает. Что делать, если я хочу вставить или добавить дочерний узел после опорного узла (сделав новый узел первым в списке)? В течение многих лет моим решением было написать следующую функцию:
Листинг 1. Неправильные методы вставки и добавления с помощью
функция InsertAfter(родительский узел, referenceNode) {
если (referenceNode.nextSibling) {
родитель.insertBefore(узел, referenceNode.nextSibling);
} еще {
родитель.appendChild(узел);
}
}
функция prependChild(родитель, узел) {
если (родитель.firstChild) {
родитель.insertBefore(узел, родитель.firstChild);
} еще {
родитель.appendChild(узел);
}
}
Фактически, как и в листинге 1, функция InsertBefore() определена для возврата к методу AppendChild(), когда ссылочный узел пуст. Поэтому вместо использования вышеуказанных методов вы можете использовать методы из листинга 2 или пропустить их и просто использовать встроенные функции:
Листинг 2. Правильный способ вставки и добавления из предыдущего
функция InsertAfter(родительский узел, referenceNode) {
родитель.insertBefore(узел, referenceNode.nextSibling);
}
функция prependChild(родитель, узел) {
родитель.insertBefore(узел, родитель.firstChild);
}
Если вы новичок в программировании DOM, важно отметить, что, хотя у вас может быть несколько указателей, указывающих на узел, этот узел может существовать только в одном месте в дереве DOM. Поэтому, если вы хотите вставить его в дерево, нет необходимости сначала удалять его из дерева, поскольку он будет удален автоматически. Этот механизм удобен при изменении порядка узлов путем простой вставки их на новые позиции.
Согласно этому механизму, если вы хотите поменять местами два соседних узла (называемых node1 и node2), вы можете использовать одно из следующих решений:
node1.parentNode.insertBefore(node2, node1);
или
node1.parentNode.insertBefore(node1.nextSibling, node1);
Что еще можно делать с DOM?
DOM широко используется на веб-страницах. Если вы посетите сайт букмарклетов (см. Ресурсы), вы найдете множество креативных коротких сценариев, которые могут переупорядочивать страницы, извлекать ссылки, скрывать изображения или Flash-объявления и многое другое.
Однако, поскольку Internet Explorer не определяет константы интерфейса узла, которые можно использовать для идентификации типов узлов, вы должны убедиться, что если вы опустите константу интерфейса, вы сначала определите константу интерфейса в сценарии DOM для Интернета.
Листинг 3. Проверка определения узла
если (!window['Узел']) {
window.Node = новый объект();
Узел.ELEMENT_NODE = 1;
Узел.ATTRIBUTE_NODE = 2;
Узел.TEXT_NODE = 3;
Узел.CDATA_SECTION_NODE = 4;
Узел.ENTITY_REFERENCE_NODE = 5;
Узел.ENTITY_NODE = 6;
Узел.PROCESSING_INSTRUCTION_NODE = 7;
Узел.COMMENT_NODE = 8;
Узел.DOCUMENT_NODE = 9;
Узел.DOCUMENT_TYPE_NODE = 10;
Узел.DOCUMENT_FRAGMENT_NODE = 11;
Узел.NOTATION_NODE = 12;
}
В листинге 4 показано, как извлечь все текстовые узлы, содержащиеся в узле:
Листинг 4. Внутренний текст
функция внутреннийтекст (узел) {
// это текстовый узел или узел CDATA?
if (node.nodeType == 3 || node.nodeType == 4) {
вернуть node.data;
}
вар я;
вар returnValue = [];
for (i = 0; i < node.childNodes.length; i++) {
returnValue.push(innerText(node.childNodes[i]));
}
return returnValue.join('');
}
Сокращения.
Люди часто жалуются, что DOM слишком многословен и что простые функции требуют большого количества кода. Например, если вы хотите создать элемент <div>, содержащий текст и реагирующий на нажатие кнопки, код может выглядеть так:
Листинг 5. «Долгий путь» к созданию <div>
функция handle_button() {
var родитель = document.getElementById('myContainer');
вар div = document.createElement('div');
div.className = 'myDivCSSClass';
div.id = 'myDivId';
div.style.position = 'абсолютный';
div.style.left = '300 пикселей';
div.style.top = '200px';
var text = "Это первый текст остальной части кода";
вар textNode = document.createTextNode(текст);
div.appendChild(textNode);
родитель.appendChild(div);
}
Если вы часто создаете узлы таким образом, ввод всего этого кода быстро вас утомит. Должно было быть лучшее решение – и оно было! Вот утилита, которая поможет вам создавать элементы, устанавливать свойства и стили элементов, а также добавлять текстовые дочерние узлы. За исключением параметра name, все остальные параметры являются необязательными.
Листинг 6. Ярлык функции elem()
function elem(name, attrs, style, text) {
var e = document.createElement(name);
если (атрибуты) {
for (ключ в атрибутах) {
если (ключ == 'класс') {
e.className = attrs[ключ];
} еще если (ключ == 'id') {
e.id = attrs[ключ];
} еще {
e.setAttribute(ключ, attrs[ключ]);
}
}
}
если (стиль) {
for (ключ в стиле) {
e.style[ключ] = стиль[ключ];
}
}
если (текст) {
e.appendChild(document.createTextNode(text));
}
вернуть е;
}
Используя этот ярлык, вы можете создать элемент <div> в листинге 5 более лаконично. Обратите внимание, что параметры attrs и style задаются с использованием текстовых объектов JavaScript.
Листинг 7. Простой способ создания <div>
функция handle_button() {
var родитель = document.getElementById('myContainer');
родитель.appendChild(elem('div',
{класс: 'myDivCSSClass', идентификатор: 'myDivId'}
{позиция: «абсолютное», слева: «300 пикселей», сверху: «200 пикселей»},
'Это первый текст остальной части кода'));
}
Эта утилита может сэкономить вам много времени, если вы хотите быстро создать большое количество сложных объектов DHTML. Шаблон здесь означает, что если у вас есть определенная структура DOM, которую необходимо часто создавать, используйте утилиту для ее создания. Это не только уменьшает объем кода, который вы пишете, но также уменьшает количество повторяющихся вырезок и вставок кода (виновников ошибок) и облегчает ясное мышление при чтении кода.
Что дальше?
DOM часто затрудняется сказать вам, какой следующий узел в порядке документа. Вот несколько утилит, которые помогут вам перемещаться между узлами вперед и назад:
Листинг 8. nextNode и prevNode.
// возвращаем следующий узел в порядке документа
функция nextNode(узел) {
if (!node) возвращает ноль;
если (node.firstChild){
вернуть узел.firstChild;
} еще {
вернуть nextWide (узел);
}
}
// вспомогательная функция для nextNode()
функция nextWide(узел) {
if (!node) возвращает ноль;
если (node.nextSibling) {
вернуть node.nextSibling;
} еще {
вернуть nextWide(node.parentNode);
}
}
// возвращаем предыдущий узел в порядке документа
функция prevNode(узел) {
if (!node) возвращает ноль;
если (node.previousSibling) {
вернуть предыдущийDeep(node.previousSibling);
}
вернуть узел.parentNode;
}
// вспомогательная функция для prevNode()
функция previousDeep(узел) {
if (!node) возвращает ноль;
в то время как (node.childNodes.length) {
узел = node.lastChild;
}
возвратный узел;
}
Легко использовать DOM
Иногда вам может потребоваться перебрать DOM, вызывая функцию на каждом узле или возвращая значение из каждого узла. Фактически, поскольку эти идеи настолько общие, DOM Level 2 уже включает расширение под названием DOM Traversal and Range (которое определяет объекты и API для итерации всех узлов в DOM), которое используется для применения функции и выбора диапазона в DOM. . Поскольку эти функции не определены в Internet Explorer (по крайней мере, пока), вы можете использовать nextNode(), чтобы сделать что-то подобное.
Здесь идея состоит в том, чтобы создать несколько простых и распространенных инструментов, а затем скомпоновать их по-разному для достижения желаемого эффекта. Если вы знакомы с функциональным программированием, это покажется вам знакомым. Библиотека Beyond JS (см. Ресурсы) развивает эту идею.
Листинг 9. Функциональные DOM-утилиты
// возвращаем массив всех узлов, начиная с startNode и
// продолжаем работу над остальной частью дерева DOM
функция listNodes(startNode) {
список вар = новый массив ();
вар узел = startNode;
в то время как (узел) {
список.push(узел);
узел = следующий узел (узел);
}
список возврата;
}
// То же, что и listNodes(), но работает в обратном направлении от startNode.
// Обратите внимание, что это не то же самое, что запуск listNodes() и
// переворачиваем список.
функция listNodesReversed(startNode) {
список вар = новый массив ();
вар узел = startNode;
в то время как (узел) {
список.push(узел);
узел = prevNode(узел);
}
список возврата;
}
// применяем функцию к каждому узлу в nodeList, возвращаем новый список результатов
карта функции (список, функция) {
вар result_list = новый массив ();
for (var я = 0; я <list.length; я++) {
result_list.push(func(list[i]));
}
вернуть список_результатов;
}
// применяем тест к каждому узлу, возвращаем новый список узлов, для которых
// тест(узел) возвращает true
функция filter(список, тест) {
вар result_list = новый массив ();
for (var я = 0; я <list.length; я++) {
if (test(list[i])) result_list.push(list[i]);
}
вернуть список_результатов;
}
Листинг 9 содержит четыре основных инструмента. Функции listNodes() и listNodesReversed() могут быть расширены до произвольной длины, аналогично методу среза() в Array. Я оставляю это вам в качестве упражнения. Еще следует отметить, что функции map() и filter() являются полностью универсальными и могут использоваться для работы с любым списком (а не только со списками узлов). Сейчас я покажу вам несколько способов их комбинирования.
Листинг 10. Использование функциональных утилит
// Список всех имен элементов в порядке документа
функция isElement(узел) {
вернуть node.nodeType == Node.ELEMENT_NODE;
}
функция nodeName(узел) {
вернуть node.nodeName;
}
var elementNames = map(filter(listNodes(document),isElement), nodeName);
// Весь текст из документа (игнорирует CDATA)
функция isText(узел) {
вернуть node.nodeType == Node.TEXT_NODE;
}
функция nodeValue (узел) {
вернуть node.nodeValue;
}
var allText = map(filter(listNodes(document), isText), nodeValue);
Вы можете использовать эти утилиты для извлечения идентификаторов, изменения стилей, поиска определенных узлов и их удаления и многого другого. Как только API-интерфейсы DOM Traversal и Range будут широко реализованы, вы сможете использовать их для изменения дерева DOM без предварительного построения списка. Они не только мощны, но и работают аналогично тому, что я подчеркнул выше.
Опасная зона ДОМ
Обратите внимание, что основной API DOM не позволяет анализировать данные XML в DOM или сериализовать DOM в XML. Эти функции определены в расширении DOM уровня 3 «Загрузка и сохранение», но они еще не полностью реализованы, поэтому не думайте о них сейчас. Каждая платформа (браузер или другое профессиональное приложение DOM) имеет свой собственный метод преобразования между DOM и XML, но кросс-платформенное преобразование выходит за рамки этой статьи.
DOM — не очень безопасный инструмент, особенно при использовании DOM API для создания деревьев, которые нельзя сериализовать как XML. Никогда не смешивайте API-интерфейсы DOM1, не относящиеся к пространству имен, и API-интерфейсы, поддерживающие пространство имен DOM2 (например, createElement и createElementNS) в одной программе. Если вы используете пространства имен, постарайтесь объявить все пространства имен в позиции корневого элемента и не переопределять префикс пространства имен, иначе все станет очень запутанным. Вообще говоря, пока вы соблюдаете свой распорядок дня, вы не спровоцируете критические ситуации, которые могут привести к неприятностям.
Если вы использовали для анализа InternalText и InnerHTML Internet Explorer, вы можете попробовать использовать функцию elem(). Создавая подобные утилиты, вы получаете больше удобства и наследуете преимущества кроссплатформенного кода. Смешивать эти два метода очень плохо.
Некоторые символы Юникода не включены в XML. Реализация DOM позволяет их добавлять, но в результате их невозможно сериализовать. К этим символам относятся большинство управляющих символов и отдельные символы в суррогатных парах Юникода. Это происходит только в том случае, если вы попытаетесь включить в документ двоичные данные, но это еще одна ошибка.
Заключение
Я рассмотрел многое из того, на что способен DOM, но DOM (и JavaScript) могут сделать гораздо больше. Изучите и исследуйте эти примеры, чтобы увидеть, как их можно использовать для решения проблем, для которых могут потребоваться клиентские сценарии, шаблоны или специализированные API.
У DOM есть свои ограничения и недостатки, но у него также есть много преимуществ: он встроен во многие приложения; он работает одинаково, независимо от того, используете ли вы технологию Java, Python или JavaScript; использовать SAX очень легко; который одновременно прост и эффективен в использовании. Все больше приложений начинают поддерживать DOM, включая приложения на базе Mozilla, OpenOffice и XMetaL от Blast Radius. Все больше и больше спецификаций требуют DOM и расширяют его (например, SVG), поэтому DOM всегда рядом с вами. Было бы разумно использовать этот широко распространенный инструмент.