Рік без JQuery

21

Від автора: відмова від «робочої конячки» під front-end розробці ще в 2014 році привів до появи більш швидкої і компактної платформи.

Я приєднався до сайту We Are Colony влітку 2014 року. Через півроку роботи ми підійшли до тієї точки розвитку, коли нам потрібно додати кілька великих функцій і переосмислити основні частини дизайну нашої платформи.
У мене було два варіанти: або переписати весь мій свіжий код, або почати все заново. Я вибрав останнє, що дозволило внести кілька великих змін в front-end стек і його залежності – однієї з залежностей, від якої я відмовився, був JQuery. Я викинув його в 2014 році.

На той момент у мене вже було кілька маленьких завершених проектів на чистому JS, але цей став першим великомасштабним додатком з потужним UI і без JQuery. Як новачок з JQuery і автор великої кількості плагінів для цієї всюдисущої бібліотеки, зараз я підійшов до певної точки і відчуваю себе винним, згадуючи всі випадки, коли я викликав легендарну функцію $() (як і безліч інших розробників, з ким я розмовляв). Я й раніше постійно намагався використовувати чистий JS скрізь, де це буде безпечно для всіх браузерів. І зараз я відчуваю, що пора особисто від себе і від усього співтовариства front-end розробників сказати прощай нашому старому другові.

За 18 місяців отримані мною уроки в процесі створення JQuery UI без виявилися вкрай цінні, і я хочу поділитися з вами деякими з них в цій статті. Але насправді написати цю статтю мене спонукав доповідь «Як не використовувати 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()

/**
* @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()

/**
* @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()

/**
* @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

var $items = $container.children(‘.item’);
$items.hide();

Vanilla JavaScript

var items = h.children(container, ‘.item’),
item = null,
i = -1;
for (i = 0; item = items; i++) {
item.style.display = ‘none’;
}

Обробка подій і делегування

Схожим чином, при делегуванні подій в JQuery є невеликі труднощі: повертається елемент, а не мета події. Для реалізації в JS буде потрібно трохи доповнити код (обидва прикладу в ознайомлювальних цілях):

jQuery

var $container = $(‘.container’);
$container.on(‘click’, ‘.btn’, function() {
// Додаємо клас active до кликнутому елементу ‘.btn’
$(this).addClass(‘active’);
});

Звернутися до оброблюваного елементу в JQuery можна за допомогою зручного слова «this».

Vanilla JavaScript

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.

Стандартне оголошення поведінки інтерфейсу в нашому додатку виглядає так:

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 нижче:

Атрибут data-ref показує, що посилання на цей елемент буде автоматично кэширована базовим поведінкою з допомогою виклику локалізованої функції querySelector() на властивості «з тире». Наприклад, елемент data-ref=»button-prev» в JS заданий this.dom.buttonPrev, а кореневий елемент записаний у вигляді this.dom.context (властивість успадковано від базового коду).

Коли посилання на DOM задана масивом за замовчуванням (як slides в прикладі), з допомогою властивості в однині (slide) і querySelectorAll() код кешує NodeList, а не просто елемент.

Хотілося б подякувати розробника Mike Simmonds з сайту Zone за ідею використання data-ref для елементів, що походять запит, замість класу – стилі і функціонал розділені.

Крім того, за допомогою спадкування прототипів ми можемо з легкістю розширити нашу поведінку додатковими властивостями і методами, використовуючи той же синтаксис:

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 вже сьогодні. Ви створите набагато більш легке, швидке додаток, а також дізнаєтесь багато нового.