В поздние дни работы с jQuery я начал замечать разные проблемы с этой библиотекой. Большинство из них основопологающие, поэтому не могут быть исправлены без потери обратной совместимости, что, конечно, важно. Я, как и многие другие, продолжал использовать библиотеку какое-то время, встречаясь с надоедливыми причудами каждый день.
Затем Daniel Buchner создал SelectorListener, и родилась идея live расширений. Я начал подумывать о создании набора функций, который позволит создавать ненавязчивые и независимые компоненты DOM, используя лучший подход. Задача была сделать обзор существующих решений и создать более понятную, тестируемую, маленькую, но в то же время самодостаточную библиотеку.
Добавление полезных функций в библиотеку
Идея live расширений способствовала разработке проекта better-dom, хотя кроме него имеются другие интересные особенности, которые делают библиотеку уникальной. Давайте сделаем их беглый обзор:
- live расширения
- нативные анимации
- встроенный шаблонизатор
- поддержка интернационализации
Live расширения
В jQuery существует понятие live событий. За кулисами они используют event delegation чтобы обрабатывать существующие и будущие элементы. Однако во многих случаях требуется большая гибкость. Например, если виджет должен при инициализации добавить дополнительные элементы в дерево документа, которые должны взаимодействовать или замещать существующие, live события не работают. Чтобы решить проблему я представляю live расширения.
Цель — объявить расширение однажды, и после этого оно должно работать для будущего контента независимо от сложности виджета. Это важная особенность, поскольку позволяет создавать веб-страницы декларативно, поэтому хорошо подходит для AJAX приложений.
Рассмотрим простой пример. Допустим, наша задача реализовать полностью кастомизируемую всплывающую подсказку. Псевдоселектор :hover
не подходит, потому что позиция тултипа меняется в зависимости от курсора мыши. Event delegation так же не подходит — слишком затратно слушать mouseover
и mouseleave
для всех элементов на странице. Здесь на сцену выходят live расширения.
DOM.extend("[title]", { constructor: function() { var tooltip = DOM.create("span.custom-title"); // устанавливаем textContent всплывающей подсказки в значение // title исходного элемента и скрываем ее изначально tooltip.set("textContent", this.get("title")).hide(); this // удаляем нативный tooltip .set("title", null) // сохраняем ссылку для более быстрого доступа .data("tooltip", tooltip) // регистрируем обработчики событий .on("mouseenter", this.onMouseEnter, ["clientX", "clientY"]) .on("mouseleave", this.onMouseLeave) // вставляем всплывающую подсказку в DOM .append(tooltip); }, onMouseEnter: function(x, y) { this.data("tooltip").style({left: x, top: y}).show(); }, onMouseLeave: function() { this.data("tooltip").hide(); } });
Наш тултип теперь можно стилизировать с помощью селектора .custom-title
в CSS:
.custom-title { position: fixed; border: 1px solid #faebcc; background: #faf8f0; }
Однако самое интересное начинается, когда новые элементы с атрибутом title
добавляются на страницу. Они подхватятся расширением без вызова какой-либо инициализирующей функции.
Live расширения самодостаточны, поэтому не нуждаются в дергании определенной функции, чтобы работать с будущим контентом. А значит могут комбинироваться с любой существующей библиотекой для DOM и упрощают логику приложения, разделяя UI код на множество маленьких независимых частей.
В заключение несколько слов о Web components. Один и разделов спецификации, под названием «Декораторы», предназначен для решения схожей проблемы. В настоящее время он использует разметку и специальный синтаксис для навешивания слушателей на дочерние элементы. Но это пока очень ранний черновик:
Декораторы, в отличие от других разделов Web Components не имеют пока спецификации.
Нативные анимации
Благодаря Apple в CSS сейчас есть хорошая поддержка анимаций. В прошлом анимации реализовывались на JavaScript с помощью setInterval
и setTimeout
. Это была классная фишка но теперь… что-то вроде плохой практики. Нативные анимации всегда будут более плавными: они обычно быстрее, требуют меньше энергии и просто не показываются в браузерах, которые их не поддерживают.
В better-dom нет метода animate
: только show
, hide
и toggle
. Чтобы захватить состояние скрытого элемента в CSS библиотека использует стандартизированный атрибут aria-hidden
.
Для иллюстрации подхода давайте добавим простую анимацию тултипу, который мы написали ранее:
.custom-title { position: fixed; border: 1px solid #faebcc; background: #faf8f0; /* анимация */ opacity: 1; -webkit-transition: opacity 0.5s; transition: opacity 0.5s; } .custom-title[aria-hidden=true] { opacity: 0; }
Внутри show
и hide
атрибут aria-hidden
меняет свое значение на false
или true
. Этого достаточно чтобы показывать анимации средствами CSS.
Больше примеров анимаций с помощью better-dom.
Встроенный шаблонизатор
HTML-строки громоздкие. Когда я начал искать замену, то нашел отличный проект Emmet. В настоящее время он достаточно популярный в качестве плагина для текстовых редакторов и имеет чистый и компактный синтаксис. Сравните:
body.append("<ul><li class='list-item'></li><li class='list-item'></li><li class='list-item'></li></ul>");
что эквивалентно
body.append("ul>li.list-item*3");
В better-dom методы, которые принимают HTML-строки в качестве аргументов, так же поддерживают emmet-аббревиатуры. Парсер аббревиатур быстрый, поэтому можно не думать о небольшой потере производительности. Так же имеется функция для прекомпиляции шаблонов, которая может быть использована по мере необходимости.
Поддержка интернационализации
Разработка UI-виджета часто требует локализацию, что не всегда простая задача. Многие решали эту проблему по-своему. С better-dom я надеюсь, что смена языка будет не сложнее изменения состояния CSS селектора.
С идеологической точки зрения переключение языка — это наподобие изменения «представления» контента. В CSS2 есть несколько псевдоселекторов, которые помогают описать такую модель: :lang
и :before
. Взгляните на код ниже:
[data-i18n="hello"]:before { content: "Hello Maksim!"; } [data-i18n="hello"]:lang(ru):before { content: "Привет Максим!"; }
Хитрость в том, что свойство content
меняется в соответствии с текущим языком, который определяется значением атрибута lang
для элемента html
. С помощью data-атрибута data-i18n
мы можем использовать более общую запись:
[data-i18n]:before { content: attr(data-i18n); } [data-i18n="Hello Maksim!"]:lang(ru):before { content: "Привет Максим!"; }
Разумеется такой CSS код не выглядит привлекательным, поэтому в better-dom два хелпера: i18n
и DOM.importStrings
. Первый используется для обновления атрибута data-i18n
с соответствующим значением, а второй локализует строки для определенного языка.
label.i18n("Hello Maksim!"); // label отображает "Hello Maksim!" DOM.importStrings("ru", "Hello Maksim!", "Привет Максим!"); // теперь если язык страницы "ru", то label будет показывать "Привет Максим!" label.set("lang", "ru"); // теперь label показывает "Привет Максим!" независимо от языка страницы
Параметризованные строки так же поддерживаются: достаточно добавить ${param}
переменные в ключевую строку:
label.i18n("Hello ${user}!", {user: "Maksim"}); // label показывает "Hello Maksim!"
Улучшение нативных API
Обычно мы хотим придерживаться стандартов. Но иногда стандарты не совсем дружелюбные. DOM очень запутанный и, чтобы сделать его приятным, нужно обернуть его в удобный API. Несмотря на многочисленные улучшения, которые сделаны разными библиотеками, кое-какие вещи можно сделать лучше:
- getter и setter
- улучшенная обработка событий
- поддержка функциональных методов
Getter и setter
Нативный DOM имеет понятия атрибутов и свойств у элемента, которые могут вести себя по-разному. Предположим на странице имеется разметка ниже:
<a href="/chemerisuk/better-dom" id="foo" data-test="test">better-dom</a>
Чтобы объяснить недружелюбие нативного DOM, давайте поработаем с ним немного:
var link = document.getElementById("foo"); link.href; // => "https://github.com/chemerisuk/better-dom" link.getAttribute("href"); // => "/chemerisuk/better-dom" link["data-test"]; // => undefined link.getAttribute("data-test"); // => "test" link.href = "abc"; link.href; // => "https://github.com/abc" link.getAttribute("href"); // => "abc"
Итак, значение атрибута равно соответствующей строке в HTML, в то время как свойство элемента с таким же именем может иметь специальное поведение, например, генерация полного URL в листинге выше. Эта разница может иногда запутывать.
На практике сложно представить, когда такое разделение может быть полезным. Более того, разработчик должен все время следить за тем, с каким значением он работает, что добавляет ненужную сложность.
В better-dom дела обстоят проще: каждый элемент имеет только умный getter и setter.
var link = DOM.find("#foo"); link.get("href"); // => "https://github.com/chemerisuk/better-dom" link.set("href", "abc"); link.get("href"); // => "https://github.com/abc" link.get("data-attr"); // => "test"
На первом шаге методы делают поиск свойства элемента и, если оно определено, используют его для операций. В противном случае работают с соответствующим атрибутом. Для буленовских атрибутов (checked
, selected
и т.д.) можно просто использовать true
или false
. Изменение этих свойств у элемента обновляет соответствующий атрибут (нативное поведение).
Улучшенная обработка событий
Обработка событий — это значимая часть кодирования для DOM. Одна фундаментальная проблема, которую я обнаружил, состоит в том, что наличие event object в слушателях элемента заставляет разработчиков, которые любят тестируемый код, мОчить первый агрумент или создавать дополнительную функцию, которая принимает используемые в этом обработчике свойства события.
var button = document.getElementById("foo"); button.addEventListener("click", function(e) { handleButtonClick(e.button); }, false);
Это на самом деле надоедает и добавляет вызов дополнительной функции. Что если выделить меняющуюся часть в качестве аргумента: это позволит избавиться от замыкания:
var button = DOM.find("#foo"); button.on("click", handleButtonClick, ["button"]);
По умолчанию обработчик событий принимает массив ["target", "defaultPrevented"]
, поэтому нет необходимости добавлять последний аргумент, чтобы прочитать эти свойства:
button.on("click", function(target, canceled) { // обрабатываем клик });
Позднее связывание так же поддерживается (рекомендую прочитать статью от Peter Michaux по теме). Это более гибкая альтернатива обычным обработчикам событий, которая, кстати, присутствует в стандарте. Может быть полезна в случаях, когда нужно делать частые вызовы методов on
и off
.
button._handleButtonClick = function() { alert("click!"); }; button.on("click", "_handleButtonClick"); button.fire("click"); // показывается сообщение "clicked" button._handleButtonClick = null; button.fire("click"); // ничего не показывается
В завершении стоит упомянуть что в better-dom нету методов наподобие и click()
, focus()
, submit()
т.п., которые присутствуют в стандарте и имеют различное поведение в браузерах. Единственные способ их вызвать — это использовать метод fire
, который выполняет поведение по умолчанию, когда ни один из обработчиков не вернул false
:
link.fire("click"); // кликает по ссылке link.on("click", function() { return false; }); link.fire("click"); // вызывает обработчик выше но не кликает по ссылке
Поддержка функциональных методов
ES5 стандартизировал несколько полезных методов для массивов, такие как map
, filter
, some
и т.д. Они позволяют проводить операции над коллекциями в стандартизированном виде. В результате сегодня имеются проекты наподобие Underscore или Lo-Dash, которые позволяют пользоваться этими методами в старых браузерах.
Каждый элемент (или коллекция) в better-dom имеет методы ниже из коробки:
each
(отличается отforEach
тем, что возвращаетthis
вместоundefined
)some
every
map
filter
reduce[Right]
var urls, activeLi, linkText; urls = menu.findAll("a").map(function(el) { return el.get("href"); }); activeLi = menu.children().filter(function(el) { return el.hasClass("active"); }); linkText = menu.children().reduce(function(memo, el) { return memo || el.hasClass("active") && el.find("a").get() }, false);
Решение некоторых проблем jQuery
Большинство проблем ниже не могут быть исправлены в jQuery без потери обратной совместимости. Еще одна причина, по которой было решено создать новую библиотеку:
- «магическая» функция
$
- значение оператора квадратных скобок
- проблемы с
return false
find
иfindAll
«Магическая» функция $
Все слышали когда-нибудь, что функция $
(доллар) — это «магия». Имя, состоящее всего из одного символа, не очень понятное, функция выглядит как встроенный в язык оператор. Именно поэтому неопытные разработчики просто вызывают ее везде, где это необходимо.
За кулисами $
— это довольно сложная функция. Частое ее выполнение, особенно внутри таких событий как mousemove
или scroll
, может быть причиной плохой отзывчивости UI.
Несмотря на многочисленные статьи, которые продвигают кэширование объектов jQuery разработчики продолжают вставлять $
. Это потому, что синтаксис библиотеки способствует такому стилю кодирования.
Другая проблема с этой функцией состоит в том, что она является ответственной за две совершенно разные задачи. Люди уже привыкли к такому синтаксису, но это нехорошая практика дизайна функции в общем случае:
$("a"); // => поиск всех элементов, которые соответствуют селектору “a” $("<a>"); // => создает элемент <a> с jQuery врапером
В better-dom зоны ответственности $-функции покрывают несколько методов: find[All]
и DOM.create
. Методы find[All]
используются для поиска элементов по CSS-селектору. DOM.create
создает новые элементы в памяти. Имена функций ясно говорят что эти функции делают.
Значение оператора квадратных скобок
Еще одна причина проблемы с слишком частыми вызовами доллар-функции — это оператор квадратных скобок. Когда создается новый jQuery-объект все связанные элементы сохраняются в numeric-свойствах. Важно заметить, что значение такого свойства содержит экземпляр нативного элемента (не jQuery-врапера):
var links = $("a"); links[0].on("click", function() { ... }); // ошибка! $(links[0]).on("click", function() { ... }); // теперь все хорошо
Из-за такой особенности каждый функциональный метод в jQuery или другой библиотеки (как Underscore) требует, чтобы текущий элемент оборачивался в $()
внутри итерационной функции. Поэтому разработчик обязан всегда помнить с каким объектом он работает: нативным или врапером, несмотря на факт, что используется библиотека для работы с DOM.
В better-dom оператор квадратных скобок возвращает объект библиотеки, поэтому можно забыть о нативных элементах. Единственный легальный способ получить к ним доступ — это использовать специальный метод legacy
.
var foo = DOM.find("#foo"); foo.legacy(function(node) { // используя библиотеку Hammer слушаем жест swipe Hammer(node).on("swipe", function(e) { // обрабатываем жест swipe }); });
Но в реальности он нужен в очень редких случаях, например, когда нужна совместимость с нативной функцией или другой DOM библиотекой (как Hammer из примера выше).
Проблемы с return false
Одна вещь, которая действительно взорвала мне мозг это странная обработка return false
в слушателях событий. В соответствии с стандартами W3C это значение должно в большинстве случаев отменять поведение по умолчанию. В jQuery return false
дополнительно останавливает event delegation!
Здесь сразу несколько проблем:
- вызов
stopPropagation()
сам по себе может создавать проблемы с совместимостью, т.к. он ломает возможность других слушателей делать их работу по возникновению такого события - большинство разработчиков (даже опытных) не ожидают такого поведения
Непонятно почему сообщество jQuery решило пойти против стандартов. И better-dom не собирается повторять эту ошибку: return false
внутри обработчика событий вызывает только preventDefault()
, как и ожидается.
find и findAll
Поиск элементов — это одна из самых дорогих операций в браузере. Две нативные функции могут использоваться чтобы реализовать его: querySelector
и querySelectorAll
. Разница между ними в том, что первая останавливает поиск после первого совпадения.
Эта особенно позволяет значительно уменьшить количество итераций в подходящих случаях. В моих тестах выигрыш в скорости может быть до 20 раз. Так же можно ожидать что разрыв увеличивается в зависимости от размера дерева документа.
jQuery имеет find
метод, который использует querySelectorAll
в общем случае. На сегодняшний день здесь нет метода, который бы использовал querySelector
, чтобы найти только первый подходящий элемент.
В better-dom есть два разных метода: find
и findAll
. Они позволяют использовать querySelector
-оптимизацию выше. Чтобы оценить потенциальный выигрыш я сделал выборку по количеству вхождений по исходному коду последнего коммерческого проекта:
find
— 103 совпадений в 11 файлахfindAll
— 14 совпадений в 4 файлах
Определенно, что метод find
горазде более популярный. Это значит, что querySelector
-оптимизация имеет место в большинстве вариантах использования, а значит может дать значительный выигрыш в производительности кода на клиенте.
Заключение
Разработка с использованием live расширений действительно упрощает жизнь на front-end. Разделение UI на множество маленьких частей помогает создавать более независимые (=надежные) решения. Но, как видно выше, better-dom не только о них (хотя это была изначальная главная цель).
Во время разработки я понял одну важную вещь: если текущие стандарты не совсем устраивают или есть идеи как можно сделать лучше — просто реализуй и докажи что они работают. И это очень весело!
Больше информации о библиотеке better-dom всегда можно найти на GitHub.
ссылка на оригинал статьи http://habrahabr.ru/post/209140/
Добавить комментарий