Основы «ненавязчивого» JavaScript

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

Давайте рассмотрим, как научиться использовать все преимущества современных браузеров, не ограничивая при этом пользователей более старых версий.

Javascript: храним отдельно

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

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

<script type=”text/javascript” src=”scripts.js”></script>

Javascript – это расширение

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

Это не означает, что мы совсем не можем использовать Javascript, это только значит, что мы можем добавлять его лишь как дополнительную возможность. Страницы должны работать и при выключенном JavaScript – это одно из основных правил «ненавязчивого» JavaScript.

Давайте рассмотрим для примера следующий HTML-код:

<form action=”/”>
     <p><label for=”login”>Логин:</label> 
    <input type=”text” name=”login” id=”login”/></p> 
    <p><label for=”password”>GПароль:</label> 
    <input type=”password” name=”password” id=”password”/></p> 
    <p><input type=”button” onclick=”checkform()” value=”Войти”/></p>
</form>

Если у пользователя отключен JavaScript (или он отрабатывает некорректно в силу особенностей различных браузеров), то зайти на сайт будет просто невозможно. И это только по причине неверного проектирования, а не ошибки на стороне сервера или неверного расположения элементов в дизайне.

Мы можем исправить этот фрагмент, заменив button на submit и добавив обработчик события submit для формы:

<p><input type=”submit” value=”Войти”/></p>

<script type=.text/javascript.>
    document.forms[0].onsubmit = checkform;
</script>

Доверять, но проверять

Множество ошибок в Javascript происходит просто потому, что разработчики слишком ленивы, чтобы проверить, доступен или нет данный объект или метод. Всегда при использовании какой-либо внешней переменной (которая не определена явно в этой функции) либо функционала, зависящего от браузера (например, DOM-методов), необходимо быть уверенным, что используемый объект или метод существует.

В качестве примера правильных проверок можно привести следующий код:

function color(object, color) { 
    if(object) { 
        if (color) { 
            if (object.style) { 
                object.style.color = color; 
            } 
        } 
    }
}

Если мы хотим убедиться, что браузер поддерживает W3C DOM, то достаточно провести следующую проверку:

if (document.getElementById) {
}

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

Доступ к элементам

Каждый XML- (а также и HTML-) документ – это дерево узлов. Узел – часть этого дерева (в качестве аналогии можно привести дерево файлов и директорий на жестком диске). Давайте посмотрим, какие функции и атрибуты мы можем использовать, чтобы перемещаться по дереву документа и выбирать необходимые нам узлы.

getElementById(‘elementID’)
возвращает элемент с идентификатором, равным elementID

getElementsByTagName(‘tag’)
возвращает массив элементов с именем tag

Естественно, мы можем смешивать и сочетать эти два варианта. Несколько примеров:

document.getElementById(‘nav’).getElementsByTagName(‘a’)[1];
// возвращает вторую ссылку внутри элемента, который имеет ID ‘nav’

document.getElementsByTagName(‘div’)[1].getElementsByTagName(‘p’)[3];
// возвращает четвертый параграф внутри второго div в документе.

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

Полезные советы

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

  • Прежде, чем обращаться к элементу, нужно убедиться, что он существует.
  • JavaScript-код не должен быть привязан к верстке, только к DOM-дереву. Лишний перевод строки может быть прочитан как новый текстовый узел, разметка может поменяться, а менять скрипты при каждом изменении дизайна не очень хочется.
  • HTML, полученный через DOM-методы, в большинстве случаев невалиден. Если мы хотим его повторно использовать, лучше всего привести его к валидному виду или применять innerHTML для всех таких случаев (задавая в качестве параметра часть валидного документа).
  • Следует избегать частого перебора элементов. Каждая операция (особенно getElementesByTagName) довольно ресурсоемка. Стоит кэшировать наиболее часто используемые элементы (подробнее о кэшировании в JavaScript рассказывается чуть далее в этой главе).
  • Не стоит проверять атрибуты, которых нет (если мы знаем верстку и знаем JavaScript-код, то в нем не должны появиться неизвестные атрибуты).
  • Нужно подходить осторожно к верстке с других сайтов. Например, при проверке className на наличие определенной строки нельзя использовать точное соответствие, только регулярные выражения (ибо атрибут этот может содержать несколько классов, разделенных пробелом).

Добавляем обработчики событий

Главная техника, которую мы используем, чтобы сохранить наш Javascript «ненавязчивым», – это хранение скрипта в отдельном файле, что предпочтительней, чем смешивать его с разметкой страницы. Чтобы исполнить функции в нашем .js файле, нам надо вызвать их, когда страница загружена (подробнее о событии загрузки страницы было рассказано в начале этой главы).

В некоторых случаях (например, при экстремальной оптимизации, глава четвертая) весь JavaScript-код может находиться в HTML-документе, заключенный в <script type=.text/javascript.>…</script>. Но это не будет означать, что мы смешиваем разметку страницы с ее обработкой, а содержание – с функциональностью. В этих случаях JavaScript-код будет полностью отделен от содержания, для которого он предназначен.

Существует возможность добавлять обработчики событий в комплект к уже существующим обработчикам. При вызове функции мы передаем ей объект, который нужно привязать к событию, тип события и имя функции.

function addEvent(object, eventType, function){ 
    if (object.addEventListener){ 
        object.addEventListener(eventType, function, false); 
        return true; 
    } else { 
        if (object.attachEvent){ 
            var r = object.attachEvent(\”on\”+eventType, function); 
            return r; 
        } else { 
            return false; 
        } 
    }
}

События – довольно сложная тема в Javascript. Для разработки простых веб-сайтов указанных примеров достаточно, но если мы переходим к разработке веб-приложений, тут ситуация многократно усложняется. Поэтому стоит быть внимательным к их функционированию в условиях отключенного или неподдерживаемого JavaScript.

Ускоряем обработку событий

Давайте рассмотрим, как можно использовать методы «ненавязчивого» JavaScript для максимального ускорения обработки событий в браузере. Мы можем уменьшить число приемников событий, которые назначены документу, путем определения одного приемника для контейнера и проверки в обработчике, из какого дочернего элемента всплыло это событие.

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

Скорее всего, ситуация будет выглядеть следующим образом.

var MenuNavigation = {
    init: function() { 
        var navigation = document.getElementById(‘mainNav’); 
        var links = navigation.getElementsByTagName(‘a’); 
        for ( var i = 0, j = links.length; i < j; ++i ) { 
            if ( /bundle/i.test(links[i].className) ) { 
                links[i].onclick = this.onclick; 
            } 
        } 
    }, 
    onclick: function() { 
            this.href = this.href + ‘?name=value’; 
            return true; 
    }
}

В этом фрагменте довольно много лишнего. Во-первых, метод getElementsByTagName просматривает каждый дочерний DOM-узел в элементе mainNav, чтобы найти все ссылки. Затем мы еще раз пробегаем по всему найденному массиву, чтобы проверить имя класса каждой ссылки. Это пустая трата процессорного времени на каждом этапе. И это замедление загрузки страницы на уровне JavaScript-логики.

Немного усложним

Можно прикрепить один-единственный обработчик событий к элементу mainNav, чтобы затем отслеживать все клики на ссылки внутри него:

var MenuNavigation = { 
    init: function() { 
        var navigation = document.getElementById(‘mainNav’); 
        navigation.onclick = this.onclick; 
    }, 
    onclick: function(e) { 
        if ( /bundle/i.test(e.target.className) ) { 
            e.target.href = e.target.href + ‘?name=value’; 
        } 
        return true; 
    }
}

Простота и элегантность данного подхода должны быть очевидны, но у него есть и некоторое количество преимуществ в плане производительности:

  • Чем меньше приемников событий прикреплено к документу, тем лучше. Они все загружаются в память и в чрезвычайных случаях могут сильно замедлить работу браузеров. Также это увеличивает число замыканий, что чревато утечками памяти. Подробнее рассказывается далее в этой главе.
  • Загружается меньше кода на странице. Одной из главных проблем для сложных веб-приложений является задержка при загрузке JavaScript для исполнения и визуализации документа. Два цикла из первого примера отсутствуют во втором.
  • «Исполнение по требованию». Второй пример выполняет немного больше действий, когда вызывается конечный обработчик событий, но это лучше, чем выполнять все действия при загрузке страницы, когда мы даже не знаем, будет ли запущен каждый конкретный обработчик событий. Ссылок на странице может быть сотни, а пользователь нажмет только одну или две из них.

Боремся с Internet Explorer

Есть одна небольшая проблема при использовании изложенного выше кода. Определение целевого элемента у события, на самом деле, не является просто вызовом e.target. В Internet Explorer необходимо использовать e.srcElement. Самым простым решением для устранения этой проблемы является небольшая функция getEventTarget. Ниже представлена наиболее актуальная версия.

function getEventTarget(e) { 
    var e = e || window.event; 
    var target = e.target || e.srcElement; 
    if (target.nodeType == 3) { // боремся с Safari 
        target = target.parentNode; 
    } 
    return target;
}

Переопределение событий в настоящее время является самой распространенной практикой, если речь заходит о большом числе обработчиков событий (например, о карте с сотнями точек, к которым назначены обработчики событий-кликов). Лучше всего для этого по умолчанию использовать простой, интуитивно понятный и хорошо оптимизированный метод для применения в качестве шаблона в программировании на стороне клиента, и он не должен требовать сотен строчек JavaScript-библиотек для своей работы.

Пойдем дальше

А что, если нам нужно добавить такой обработчик на все ссылки (или почти на все)? Правильно: тогда для контейнера всех этих ссылок стоит выбрать document.body. Ниже пример кода, который позволяет так сделать.

var MenuNavigation = { 
    init: function() { 
        document.body.onclick = function(e) { 
            var target = getEventTarget(e); 
            if ( target && /bundle/i.test(target.className) ) { 
                target.href += ‘?name=value’; 
            } 
            return true; 
       }; 
    } 
    var getEventTarget = function(e) { 
        var e = e || window.event; var target = e.target || e.srcElement;
// боремся с Safari и вложенностью 
        while ( !target.href || target.nodeType == 3 ) { 
            target = target.parentNode; 
        }(); 
    return target; 
    }
}
window.onload = MenuNavigation.init;

Если мы собираемся обрабатывать все ссылки, то нужно учесть, что в них могут быть вложены и картинки, и другие теги, поэтому добавлено рекурсивное «всплытие» ссылки: проверяется родитель объекта, на котором сработало событие, и если у него не определен атрибут href, то перебор продолжается, иначе возвращаем искомый объект. Вложение ссылок друг в друга запрещено стандартами, так что, если мы сами же проектируем HTML-код, то бояться нечего.

Обработка событий в браузерах

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

node.onclick = function(){
}

Если нужно несколько событий, или просто «осторожничаем», то можно воспользоваться следующей распространенной записью:

if (node.addEventListener) 
    node.addEventListener(‘click’, function(e){}, false);
else 
    node.attachEvent(‘onclick’, function(){});

Или таким модицифицированным вариантом (меньше символов):

if (node.attachEvent) 
    node.attachEvent(‘onclick’, function(){});
else 
    node.addEventListener(‘click’, function(e){}, false);

Можно также использовать отдельную переменную для обработчика события:

var addEvent = node.attachEvent || node.addEventListener;
addEvent(/*@cc_on ‘on’+@*/’click’, function(){}, false);

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

node[/*@cc_on !@*/0 ? ‘attachEvent’ : ‘addEventListener’] 
    (/*@cc_on ‘on’+@*/’click’, function(){}, false);

Работаем с событиями

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

node[/*@cc_on !@*/0 ? ‘attachEvent’ : ‘addEventListener’] 
    (/*@cc_on ‘on’+@*/’click’, function(e){ 
        var target = e.target || e.srcElement

// или

if (!e.target) { 
    e.target = e.srcElement
}

// или, если нам надо всего один раз
(e.target || e.srcElement).tagName

// true везде кроме IE, в котором this === window
this == node;
// отменяем всплытие события
if (e.stopPropagation) 
    e.stopPropagation()
else 
    e.cancelBubble

// или просто используем вариант, который
// для совместимости работает во всех браузерах.
e.cancelBubble = true

// убираем действие по умолчанию (в данном случае клик)
if (e.preventDefault) 
    e.preventDefault()
else 
    e.returnValue = false
// при attachEvent (как здесь) работает только в IE;
// при назначении напрямую (node.onclick) – везде return false;

}, false):

Материалы близкой тематики:

Posted in Разгони свой сайт.