Скальпель, сестра!

11 октября 2008 года, 12:38

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

Давайте попробуем сегодня достичь подобного результата. Думаю, что у нас всё получится (впрочем, как всегда).

Теоретические изыскания

Как же можно облегчить жизнь тому, кто хочет каким-либо образом изменить, дополнить или перекроить Web-документы? Можно попробовать разделить документ на слои, чтобы легче было работать с каждым из них. Чтобы легче было понять смысл слоёных документов, представим себе следующую картину: у нас есть документ, содержащий в себе заголовок, основную часть и подвал. Мы хотим добавить к заголовку ещё и подзаголовок, но у нас сложилась такая ситуация, что мы не можем напрямую изменить содержимое документа (например, мы пишем расширение Firefox, или скрипт Greasemonkey и подобные) или нам нужно ненавязчиво расширить функциональность какого-то блока нашего документа (например, в экспериментальных целях). В обычной ситуации, мы бы перебирали всё DOM-дерево, чтобы добраться до нужного нам узла, после этого использовали ещё дюжину DOM-функций для дополнения искомого элемента до нужного нам представления. Согласитесь, что выполнять такое каждый раз — достаточно утомительное занятие. Как же избежать подобного?

Сделаем так: на основной странице ключевые элементы обычно всегда определяются по атрибуту id. Этим мы и воспользуемся: будем через AJAX подгружать нужный нам XML-файл, в котором будет описание слоя-настилки на наш документ. Посмотрим пример такого слоя:

<?xml version="1.0" encoding="utf-8" ?> <collection> <overlay name="Header overlaying" id="header" insert="end" type="plain"> <![CDATA[<h2>Subtitle</h2>]]> </overlay> </collection>

Данный слой накладывается на элемент с атрибутом id, равным header после всех существующих там элементов (атрибут insert со значением end). Само содержимое нового слоя содержится в специальном элементе CDATA, чтобы парсер не воспринимал всё содержимое элемента как XML, а считал его обычным текстом (при этом обязательно использование атрибута type со значением plain). С другой стороны, мы можем записывать содержимое слоя-настилки в виде XML, но тогда потребуется дополнительная процедура обработки содержимого слоя-настилки для превращения всех XML-элементов в XHTML-элементы.

Посмотрим на наш основной документ:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html lang="ru" xml:lang="ru" xmlns="http://www.w3.org/1999/xhtml"> <head> <title>JsOverlay</title> </head> <body> <div id="header"> <h1>Page title</h1> </div> <div id="content"> <p> This is page content </p> </div> <div id="footer"> <p> Footer, footer </p> </div> </body> </html>

Всё становится предельно понятно: наш слой при наложении на документ образует симбиоз элементов, изменяя структуру документа до нужной нам:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html lang="ru" xml:lang="ru" xmlns="http://www.w3.org/1999/xhtml"> <head> <title>JsOverlay</title> </head> <body> <div id="header"> <h1>Page title</h1> <!-- Вот он, наш добавленный элемент --> <h2>Subtitle</h2> </div> <div id="content"> <p> This is page content </p> </div> <div id="footer"> <p> Footer, footer </p> </div> </body> </html>

Маленькая деталь: предыдущий пример немного лукавит, так как перед тем, как наложить слой на документ, он оборачивается в элемент div с установленным атрибутом id для того, чтобы позже можно было бы проводить операции с данным слоем (удалять, например).

Что-ж, перейдём к практической части нашего повествования.

Практические изыскания

Сделаем для нашей теории небольшую практическую реализацию в качестве примера. Назовём наш класс-синглтон JSOverlay и создадим для него отдельное пространство имён:

var JSOverlay = {

Далее, нам понадобится ассоциативный массив для хранения загруженных слоёв (чтобы их можно было контролировать после загрузки):

Overlays : {},

Мы уже сейчас можем реализовать метод для получения слоя (или слоёв, так как одному элементу может быть назначено несколько слоёв):

GetOverlayById : function(overlayid) { if (JSOverlay.overlays[overlayid] != null) { //Возвращаем всё, что найдено return JSOverlay.overlays[overlayid]; } else { //Ничего не найдено return false; } },

Опишем класс слоя, чтобы было легче управлять столь одинаковыми сущностями:

Overlay : function() {

Нам понадобится структура информации о слое, которая должна состоять из имени слоя, его типа и позиции. Создадим её:

this.meta = { "name" : null, "insert" : "end", "type" : null };

Чтобы не разбрасывать ассоциированные элементы, заключим их также в одну структуру:

this.elements = { //Контейнер слоя "container" : null, //Содержимое слоя (CDATA или чистый XML) "content" : null, //Элемент, который мы дополняем (исходный элемент) "element" : null };

Вслед за этим, реализовываем методы, необходимые для утилизации возможностей слоя:

this.attach = function() { //Создаём контейнер this.elements.container = document.createElement("div"); //Атрибут id контейнера равен jsoverlay-<id-исходного-элемента> this.elements.container.id = "jsoverlay" + ((this.elements.element.id != "") ? "-" + this.elements.element.id : "-body-element"); //Заполняем его содержимое if (this.meta.type == "plain") { for (var i = 0; i < this.elements.content.childNodes.length; i++) { if (this.elements.content.childNodes[i].nodeType == 4) this.elements.container.innerHTML = this.elements.content.childNodes[i].nodeValue; } } else if (this.meta.type="xml"> { //Если необходимо, это также можно реализовать } //Прибавляем контейнер в нужное местоположение switch (this.meta.insert) { case "end": this.elements.element.appendChild(this.elements.container); break; case "start": this.elements.element.insertBefore(this.elements.container, this.elements.element.firstChild); break; } }

И ещё один метод, для отделения нового слоя от исходного элемента:

this.remove = function() { //Удаляем грязно this.elements.element.removeChild(this.elements.container); //Обнуляем контейнер this.elements.container = null; }

Заметьте, что мы не удаляем весь слой, а только то, что он внедряет на страницу. Осталось только завершить наш класс:

},

Двигаемся дальше. Теперь нам нужен метод уже пространства имён JSOverlay для загрузки нашего слоя. По сути, мы разделим данный метод на два метода: собственно загрузка и обработка XML-данных вместе с созданием, добавлением и активацией слоя. Начнём с загрузки через XMLHttpRequest (нет ничего проще):

LoadOverlay : function(overlayfile) { var Request = false; if (window.XMLHttpRequest) { Request = new XMLHttpRequest(); } else if (window.ActiveXObject) { try { Request = new ActiveXObject("Microsoft.XMLHTTP"); } catch (Exp) { try { Request = new ActiveXObject("Msxml2.XMLHTTP"); } catch (ExpCascade) { //Очень жаль Request = false; } } } //Что?! Браузер не поддерживает XMLHttpRequest? С ума сойти! if (!Request) return false; //Создаём GET-запрос к XML-файлу с нашим слоем Request.open("GET", overlayfile); Request.onreadystatechange = function() { //Вроде всё хорошо if (Request.readyState == 4) { //Ещё раз убеждаемся, что всё действительно хорошо if (Request.status != 200 && Request.status != 304) { //Заберите меня отсюда! return; } //Вызываем обработчик XML-данных JSOverlay.ParseOverlay(Request.responseXML); } } //Отсылаем запрос Request.send(null); },

Понятно, что с помощью вышеописанного метода мы сможем загружать слои как JSOverlay.LoadOverlay("overlay/my.xml");. Естественно, путь, который загружается с помощью данного метода, должен существовать, иметь Content-Type text/xml (или один из подобных) и содержать в себе валидный и соответствующий XML.

Мы не реализовали последний метод, сделаем же это:

ParseOverlay : function(XmlOverlayDocument) { try { //Получаем набор слоёв var XmlOverlays = XmlOverlayDocument.getElementsByTagName("overlay"); //Один файл может содержать несколько слоёв for (var i = 0; i < XmlOverlays.length; i++) { //Текущий слой var XmlCurrentOverlay = XmlOverlays[i]; //Создаём новый экземпляр класса var Overlay = new JSOverlay.Overlay(); //Метаинформация Overlay.meta.name = XmlCurrentOverlay.getAttribute("name"); Overlay.meta.insert = XmlCurrentOverlay.getAttribute("insert"); Overlay.meta.type = XmlCurrentOverlay.getAttribute("type"); //Получаем id исходного элемента var element_id = XmlCurrentOverlay.getAttribute("id"); //Типичные ситуации switch (element_id) { //Нужно присоединить к body case "body": element_id = document.body; break; default: element_id = document.getElementById(element_id); break; } if (element_id != null) { //Устанавливаем исходный элемент Overlay.elements.element = element_id; //Устанавливаем содержимое Overlay.elements.content = XmlCurrentOverlay; //Подключаем Overlay.attach(); if (JSOverlay.overlays[element_id.id] == null) { JSOverlay.overlays[element_id.id] = []; } //Добавляем новый слой в массив слоёв для данного элемента JSOverlay.overlays[element_id.id].push(Overlay); } else { continue; } } } catch (excp) { return false; } }

Готово! Всё, что получилось, можно посмотреть на странице-примере, открыв её исходные коды.

Что можно добавить?

Чтобы облегчить себе жизнь в некоторых ситуациях, можно превратить overlay.js в какой-нибудь серверный скрипт, который будет генерировать JavaScript-код для подключения разных слоёв. К примеру, можно сделать так: при обращении к /overlay/add/ генерируется JavaScript для подключения слоя с именем .

Выводы

Представленный прототип уже сейчас может выполнять возложенные на него обязанности, но его можно расширять до бесконечности: поддержка стилей, встроенных в расширение скриптов (хотя, этого можно достичь, просто прописав в нём элемент script, который будет подключать внешний файл с кодом), дерева DOM (для поиска элементов не только по id, но и по другим отличительным чертам, например, по атрибуту class или и вовсе по произвольному атрибуту элементов). Можно даже периодически заглядывать на вышеуказанную страницу с примером, так как иногда там будет появляться новая версия небольшого скрипта.

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

Мнения (7)

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

Я тоже знаю!

Для обращения к человеку используйте символ @, после которого следует имя того, к кому обращаетесь (пробелы заменяются на знак подчёркивания). Если вам интересно, можете подписаться на комментарии по RSS или по эл. почте. Ведите себя достойно, вы же не роботы, правда?

Вы можете использовать следующие XHTML-элементы в разметке комментария: strong, em, span[class=crossline], a[href=uri], code[type=язык], blockquote, ul и ol. В качестве языка кода может быть указан, например, javascript или css.