От автора: отказ от «рабочей лошадки» во front-end разработке еще в 2014 году привел к появлению более быстрой и компактной платформы.
Я присоединился к сайту We Are Colony летом 2014 года. Спустя полгода работы мы подошли к той точке в разработке, когда нам потребовалось добавить несколько больших функций и переосмыслить основные части дизайна нашей платформы.
У меня было два варианта: или переписать весь мой свежий код, или начать все заново. Я выбрал последнее, что позволило внести несколько крупных изменений в front-end стек и его зависимости – одной из зависимостей, от которой я отказался, был JQuery. Я выбросил его в 2014 году.
На тот момент у меня уже было несколько маленьких завершенных проектов на чистом JS, но этот стал первым крупномасштабным приложением с мощным UI и без JQuery. Как новичок с JQuery и автор большого количества плагинов для этой вездесущей библиотеки, сейчас я подошел к определенной точке и чувствую себя виновным, вспоминая все случаи, когда я вызывал легендарную функцию $() (как и множество других разработчиков, с кем я разговаривал). Я и раньше постоянно старался использовать чистый JS везде, где это будет безопасно для всех браузеров. И сейчас я чувствую, что пора лично от себя и от всего сообщества front-end разработчиков сказать прощай нашему старому другу.
За 18 месяцев полученные мной уроки в процессе создания UI без JQuery оказались крайне ценны, и я хочу поделиться с вами некоторыми из них в этой статье. Но на самом деле написать эту статью меня побудил доклад «Как не использовать JQuery» с недавней встречи front-end London, где был и я. Встреча была довольно информативной, и особое внимание на ней уделили одной неправильной концепции, о которой я услышал от нескольких людей незадолго до встречи – что ES6 спасет нас от JQuery (сразу после излечения рака и победы над мировой бедностью). Я сразу же вспомнил, как недавно я разговаривал с другом разработчиком, который говорил мне, что его команда ждет не дождется избавиться от JQuery «как только ES6 станет более распространенным».
«особое внимание на ней уделили одной неправильной концепции… что ES6 спасет нас от JQuery»
Я до конца не понимаю, откуда вообще появилась эта идея, и хорошо, что она не особо популярно, но данную проблему стоит разобрать в любом случае. По моему мнению, ES6, по большей части столь необходимое синтаксическое улучшение языка JavaScript и JQuery, это библиотека манипуляции DOM с красивым API. У ES6 и JQuery, на самом деле, общего совсем немного, и в первую очередь я хотел написать эту статью, чтобы доказать, что вы можете спокойно отказаться от JQuery, и для этого вам не понадобиться переходить на ES6 или Babel.
Вы можете спросить, а зачем вообще отказываться от JQuery? Во-первых, это перегрузка приложения и время загрузки (особенно на слабых устройствах и медленных соединениях); во-вторых, производительность UI и адаптивность (опять же на слабых устройствах); и последнее, избавление от ненужной абстракции, что позволит вам лучше понять принцип DOM, браузер и его API.
Если и была хоть одна причина оставить JQuery, то, возможно, это поддержка IE8, однако я надеюсь все согласятся, что эти времена благополучно прошли (а если это для вас не такая и причина, то вы мне уже нравитесь). В IE8 не было браузерного DOM API, которое теперь и помогло нам избавиться от JQuery; вещи типа Element.querySelectorAll(), Element.matches(), Element.nextElementSibling и Element.addEventListener() теперь есть во всех браузерах.
В IE9 и выше все еще остаются проблемы, однако данные браузеры более-менее предсказуемы в вопросе «основного» DOM API, как я его называю, которое нужно для написания приложений с тяжелым UI без использования JQuery и без подключения несчетного количества полифилов и библиотек (к сожалению с одним исключением — Element.classList в IE9).
Тем не менее, никто не будет отрицать, что вместе с JQuery идет целый набор полезных функций, а также инструментов для таких вещей, как Ajax и анимация. И в этот момент становится интересно, что включить в свой front-end набор, а что нет.
Хелпер функции
Я понял, что, отказавшись от JQuery, мне выпала прекрасная возможность самому написать парочку хелпер функций и немного больше изучить браузеры и DOM. Это был самый ценный урок для меня. Статический класс хелпер методов (я называю его «h») охватывает такие базовые вещи, как запрос дочерних или родительских элементов, расширение объектов и даже Ajax, а также множество других вещей, не относящихся к DOM.
Может показаться, что это попытка переписать JQuery, однако цель была совершенно другая. Эта небольшая коллекция удобных хелпер методов является лишь крошечной частью всего функционала JQuery без возможности оборачивать элементы в контейнеры или лишней абстрактности. На самом деле нативные браузерные API позволяют нам взаимодействовать с DOM без подключения JQuery, а эти функции заполняют те небольшие пропуски, которые были, когда я только приступил к проекту.
Ниже представлены несколько из тех хелпер функций. Те, которые я посчитал нужными и интересными для обучения. Я не стал их записывать в таком формате, чтобы любой читающий смог их скопировать к себе в проект – они вам, скорее всего даже не нужны. Я показал данные функции, чтобы проиллюстрировать, насколько легко можно решить проблему обхода DOM с помощью вышеупомянутых API.
.children()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
/** * @param {Element} el * @param {string} selector * @return {Element[]} */ h.children = function(el, selector) { var selectors = null, children = null, childSelectors = [], tempId = ''; selectors = selector.split(','); if (!el.id) { tempId = '_temp_'; el.id = tempId; } while (selectors.length) { childSelectors.push('#' + el.id + '>' + selectors.pop()); } children = document.querySelectorAll(childSelectors.join(', ')); if (tempId) { el.removeAttribute('id'); } return children; }; |
Возвращает все дочерние элементы выбранного тега при совпадении по селектору.
.closestParent()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
/** * @param {Element} el * @param {string} selector * @param {boolean} [includeSelf] * @return {Element|null} */ h.closestParent = function(el, selector, includeSelf) { var parent = el.parentNode; if (includeSelf && el.matches(selector)) { return el; } while (parent && parent !== document.body) { if (parent.matches && parent.matches(selector)) { return parent; } else if (parent.parentNode) { parent = parent.parentNode; } else { return null; } } return null; }; |
Возвращает ближайший родительский элемент для заданного тега при совпадении по селектору, опционально можно включить сам элемент в выборку.
.index()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
/** * @param {Element} el * @param {string} [selector] * @return {number} */ h.index = function(el, selector) { var i = 0; while ((el = el.previousElementSibling) !== null) { if (!selector || el.matches(selector)) { ++i; } } return i; }; |
Возвращает номер заданного элемента по отношению к соседним, опционально выборку соседних элементов можно ограничить по заданному селектору.
С 2014 года я узнал, что для h.closestParent() теперь есть нативный эквивалент Element.closest(), а для h.children() эквивалент в форме псевдокласса ‘:scope’. С помощью данного псевдокласса в запросе можно ссылаться на сам же элемент (т.е. .querySelectorAll(‘:scope > .child’). Пока данные функции не поддерживаются повсеместно, однако интересно наблюдать за тем, с какой скоростью API подхватывают их (часто под влиянием JQuery). Поскорее бы уже отрефакторить эти две функции в нашем приложении.
Следует сказать, что я не стал включать в статью функцию h.extend(), которую сам часто использую для расширения, объединения и клонирования объектов, из-за ее сложности и длины (аналог JQuery $.extend). Мы не используем каких-либо дополнительных библиотек типа Underscore или Lodash, поэтому встроенная поддержка расширяемости была критична для нашего приложения. На Stack Overflow есть множество постов, в которых говорится, как реализовать данный функционал, однако я за собой замечаю, как с ростом потребностей постоянно улучшаю данную функцию (к примеру, копирование геттеров и сеттеров, а также глубокое копирование массивов).
За последние пару лет при работе с чистым JS мне часто помогал прекрасный ресурс You Might Not Need jQuery.
Циклы
Без JQuery мне очень не хватает одной вещи – представления коллекции объектов в виде массива, что сильно облегчает операции над несколькими элементами. Без JQuery для реализации того же функционала вам придется всецело полагаться на циклы. Но с другой стороны можете не сомневаться, от этого вы получите самый большой прирост производительности — об этом я узнал еще давно, когда пытался оптимизировать время выполнения в MixItUp. Затратную по производительности функцию $.each можно заменить на обычные циклы без выполнения каких-либо функций вообще.
jQuery
1 2 |
var $items = $container.children('.item'); $items.hide(); |
Vanilla JavaScript
1 2 3 4 5 6 7 |
var items = h.children(container, '.item'), item = null, i = -1; for (i = 0; item = items[i]; i++) { item.style.display = 'none'; } |
Обработка событий и делегирование
Схожим образом, при делегировании событий в JQuery есть небольшие трудности: возвращается элемент, а не цель события. Для реализации в JS потребуется немного дополнить код (оба примера в ознакомительных целях):
jQuery
1 2 3 4 5 6 7 |
var $container = $('.container'); $container.on('click', '.btn', function() { // Добавляем класс active к кликнутому элементу '.btn' $(this).addClass('active'); }); |
Обратиться к обрабатываемому элементу в JQuery можно с помощью удобного слова «this».
Vanilla JavaScript
1 2 3 4 5 6 7 8 9 10 |
var container = document.querySelector('.container'); container.addEventListener('click', function(e) { var target = e.target, button = h.closestParent(target, '.btn', true); if (button) { button.classList.add('active'); } }); |
Без JQuery мы скоро заметим, что кликнутый нами элемент не всегда будет тем, что мы ожидаем. Во втором примере кликнутый элемент или событие «target» (e.target) может быть кнопкой, элементом внутри кнопки или совершенно несвязанным с ней элементом. В таких ситуациях функция closestParent() бесценна (см. выше).
Необходимая абстракция
Как вы могли заметить, при переходе на обычный JS с JQuery теряется краткость записи, но такого не должно быть. Мы можем поступить также, как JQuery абстрагирует различные длинные и повторяющиеся части функционала в простой API, просто API будет больше адаптировано под наше приложение.
Недостатком полностью универсальных API, как JQuery, служит их вес. В таких библиотеках есть код на все случаи жизни. К примеру, возможность передавать в метод параметры в любом порядке, или вовсе их не передавать, но в таком случае, чтобы метод не нарушил работу других функций. Зная рамки нашего приложения, можно писать более эффективные абстракции без перегруза. В то же время в примере выше мы уже видели, что в обычном JS вызвать и обработать событие не так и сложно, но это не всегда красиво.
«Работа с DOM напрямую на самом низком уровне, возможно, вдохновит вас на рефакторинг других частей кода»
Естественно, вам никто не запрещает использовать JQuery и писать отличный API для вашего приложения, однако работа с DOM напрямую на самом низком уровне, возможно, вдохновит вас на рефакторинг других частей кода. Возьмем наш пример с многоразовыми компонентами UI:
Пример с UI компонентом
В нашем приложении каждый UI компонент имеет свой класс, который мы называем «поведением» (первоначально, метод мне показал парень с сайта We Are Colony Sam Loomes, с этим методом сегодня мы и будем работать). Мы были убеждены в работе концепции «ненавязчивого» JavaScript’а, я и сейчас в нее верю. Нам очень нравилась идея дискретных, автономных компонентов, однако, например, размытие HTML и JS в шаблонах Angular нам казалось не совсем правильным, поэтому мы старались избегать данного подхода. В то же время мы поняли, что модификация упрямого фреймворка в нашу уникальную архитектуру платформы потребует полного его взлома, а конечный результат будет довольно избыточным.
Поэтому мы решили создать собственное решение, и основная идея была в том, что JS код UI не должен быть тесно связан с разметкой. Наш код интерфейса эффективно работал по принципу прогрессивного улучшения и применялся к любой разметке, главное, чтобы элемент содержал «ключевой» элемент DOM, описанный в поведении.
Чтобы описать абстрактное поведение интерфейса я создал функцию Behavior.extend() с простым публичным интерфейсом. Интерфейс был создан для расширения «базового» поведения прототипа и абстрагирования от таких однообразных вещей, как наследование прототипов, кэширование ссылок на элемент, а также прикрепление события при любом упоминании определенного поведения в DOM.
Стандартное объявление поведения интерфейса в нашем приложении выглядит так:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
var Slider = Behavior.extend({ // Свойства, заданные в конструкторе "State" // используются для хранения внутренних данных, нужных для кода // и самих методов State: function() { this.totalSlides = -1; this.activeIndex = -1; this.isSliding = false; }, // Свойства конструктора "Dom" используются для кэша // ссылок к любым элементам или nodeLists необходимых // для работы поведения интерфейса Dom: function() { this.buttonPrev = null; this.buttonNext = null; this.slides = []; }, // В массиве событий хранятся все элементы, которым мы хотим навесить // события, вместе с их обработчиками events: [ { el: 'buttonPrev', on: ['click'], handler: 'handleButtonPrevClick' }, { el: 'buttonNext', on: ['click'], handler: 'handleButtonNextClick' } ] }, { // Этот объект - новый "прототип" поведения, // в котором заданы все классы и методы: /** * @return {Promise} */ init: function() { // запускаем любой код инициализации this.totalSlides = this.dom.slides.length; this.activeIndex = 0; }, /** * @param {Event} e * @return {void} */ handleButtonPrevClick: function(e) { // переходим к предыдущему слайду }, /** * @param {Event} e * @return {void} */ handleButtonNextClick: function(e) { // переходим к следующему слайду } }); |
При запуске приложения мы просеиваем DOM на наличие элементов с атрибутом data-behavior и querySelectorAll(). Когда поведение элемента инициировано, ссылка на этот элемент автоматически кэшируется, и к нему привязывается событие. Рассмотрим для примера код HTML ниже:
1 2 3 4 5 6 7 8 9 10 |
<div data-behavior="slider"> <div> <div data-ref="slide"></div> <div data-ref="slide"></div> ... </div> <button type="button" data-ref="button-prev"></button> <button type="button" data-ref="button-next"></button> </div> |
Атрибут data-ref показывает, что ссылка на этот элемент будет автоматически кэширована базовым поведением с помощью вызова локализованной функции querySelector() на свойстве «с тире». К примеру, элемент data-ref=»button-prev» в JS задан как this.dom.buttonPrev, а корневой элемент записан в виде this.dom.context (свойство унаследовано от базового кода).
Когда ссылка на DOM задана массивом по умолчанию (как slides в примере), с помощью свойства в единственном числе (slide) и querySelectorAll() код кэширует NodeList, а не просто элемент.
Хотелось бы поблагодарить разработчика Mike Simmonds с сайта Zone за идею использования data-ref для элементов, наследующих запрос, вместо класса – стили и функционал разделены.
Кроме того, с помощью наследования прототипов мы можем с легкостью расширить наше поведение дополнительными свойствами и методами, используя тот же синтаксис:
1 2 3 4 5 |
var TextInput = Input.extend({ ... // новые свойства }, { ... // новые методы }); |
Данный метод крайне полезен при работе с полями формы, где создается базовое поведение полей input, содержащее методы типа валидации, что потом можно расширить в отдельные классы со специальными типами полей и разным UI (к примеру, группы радио кнопок или текстовые инпуты).
При перерисовке секции DOM (например, если как-то изменилось состояние приложения) для очистки от мусора любые поведения в нем уничтожаются вызовом специального метода. Ссылки на элементы удаляются, а события отвязываются.
И снова отсутствие чего-то наподобие JQuery .off() со своим пространством имен развязывает нам руки, мы можем написать свой вариант и вообще не думать о привязке события. JQuery уже дал нам мощный синтаксис (хотя и не стандартизированный), но, в целом, он не решает крупные проблемы привязки событий. Однако в контексте определенного приложения если что-то можно полностью автоматизировать, это необходимо сделать.
Метод с UI компонентом всего лишь один пример того, что отказ от JQuery не означает утомительную работу с DOM и низкоуровневыми API. Кроме того, он данный метод вдохновил нас на чистоту кода. Используя только абстракции, необходимые вашему приложению, вы сводите к минимуму избыточность и повторение кода.
Библиотеки
Для добавления функционала типа анимации, фильтров и слайдеров мы хотели найти как можно более специфичную и самую легкую библиотеку, написанную на обычном JS, а не более монолитную как JQuery.
Для анимации мы выбрали отличную библиотеку Velocity. Мы склонны использовать CSS переходы ко всему что только возможно, и эта библиотека время от времени нам помогала.
Для горизонтального слайдера мы взяли уже iScroll, jQuery-free библиотеку с отличным API программного скроллинга.
Для фильтров, постраничной навигации, модальных окон и множества другой анимации мы взяли мою MixItUp 3 (следующий релиз MixItUp на чистом JS).
Весь наш front-end полностью асинхронен и «в перспективе» очень тяжел, поэтому мы использовали библиотеку Q. Эту библиотеку мы взяли из ES6 и откажемся от нее, как только это станет возможным.
В нашем стеке есть еще парочка библиотек, написанных на чистом JS. Они не обязательно относятся к UI или DOM, но их все же стоит упомянуть. Среди более значимых Google Shak для адаптивного битрейта видео и DRM, RequireJS для модульной загрузки и группировки, Handlebars для создания шаблонов и Moment для форматирования даты.
Синтаксис
Что касается синтаксиса и самого JS, мы пока не видим причины переходить на ES6 и транспиллер. В целях повышения производительности я предпочитаю писать код сразу как можно ближе к конечному, поэтому я бы лучше не прибегал к абстракции Babel или Traceur, пока поддержка в ES6 не станет более распространенной. Более того, мне кажется в ES5 есть замечательные функции, которым стоит уделить больше внимания – в частности, геттеры, сеттеры и методы статических объектов типа Object.seal() и Object.freeze(). Для данных функций есть множество способов применения, однако я считаю, что полезнее всего они будут при обеспечении большей строгости и безопасности структур данных в конструкторах.
Выше в примере с UI поведениями мы вызывали Object.seal() как для конструктора State, так и для Dom, чтобы убедиться, что все свойства должны быть заданы в конструкторе. Данный способ также помогает отлавливать опечатки в названиях свойств во время разработки.
В IE9 и выше почти все функции ES5 доступны нативно, так что нет причин отказываться от них. ES6 это огромный шаг вперед для JavaScript, который превращает его во взрослый и солидный язык программирования, однако его плохая поддержка не должна помешать вам отказаться от JQuery.
А есть ли место для JQuery?
Мне представилась возможность посвятить все наши ресурсы одному продукту на очень долгий промежуток времени, однако средним агентствам или заказчикам с фриланса не нужна такая высокая степень экспериментирования. JQuery все еще позволяет разработчикам писать очень мощный код парой строк, а для среднего сайта больше и не нужно (особенно, когда время разработки ограничено).
Вдобавок к этому, дизайн jQueryAPI навсегда должен остаться для нас источником вдохновения. В разработке программного обеспечения мы должны стремиться к такой же простоте и гибкости. Эти два фактора внесли огромный вклад в победу JQuery над другими библиотеками, такими как Mootools и YUI, а также позволяют множеству новичков разобраться с азами JavaScript (в том числе и мне). Как John Resig сказал в своем посте о десятилетии JQuery:
Меня радует, что, видимо, еще осталось место для простых дизайнов API в этом мире
И я считаю, что именно этот урок мы должны вынести из JQuery, неважно будет он дальше использоваться или нет. Во многом переход к чистому JS оголяет безобразную работу с DOM напрямую и показывает недостатки родных Element объектов – недостатки, которые Resig так великолепно решил в jQuery API.
С другой стороны, полученные мной знания повысили мой уровень, как разработчика, а созданные инструменты открыли мне глаза и дали уверенность и понимание чистого JS. Единственный сценарий, где я лично предпочту воспользоваться JQuery, это проект с поддержкой IE8. И это не критика JQuery, а просто признак того, что наши технологии и браузеры прошли долгий путь от фрагментированного и нестандартизированного мира jQuery v1.0.0 до сегодняшних дней.
Так что, если вы собираетесь работать над проектом, в котором можно поэкспериментировать и которому не нужна поддержка устарелых браузеров, я вам настоятельно рекомендую сделать шаг вперед и сказать прощай JQuery уже сегодня. Вы создадите намного более легкое, быстрое приложение, а также узнаете много нового.
Автор: Patrick Kunka
Источник: //blog.wearecolony.com/
Редакция: Команда webformyself.