Донецкий техникум промышленной автоматики

Dev.Opera - У пошуках ідеального JavaScript-фреймворку

  1. абстракції небезпечні
  2. відсутній конструктор
  3. Робота з DOM
  4. Обробка подій DOM
  5. управління залежностями
  6. шаблони
  7. Шаблон визначений в <script>
  8. Шаблон завантажується через AJAX
  9. Шаблон - частина розмітки сторінки
  10. Шаблон - НЕ HTML
  11. Наостанок про шаблони
  12. модульність
  13. Відкрите API
  14. тестованих
  15. документація
  16. Висновок

У наші дні в області фронтенд-розробки є безліч фреймворків і бібліотек. Якісь із них хороші, якісь ні. Часто нам подобається тільки певний принцип або певний синтаксис. Правда в тому, що універсального інструменту немає. Ця стаття про майбутній фреймворк - фреймворк, якого ще не існує. Я резюмував переваги і недоліки деяких популярних JavaScript-фреймворків і наважився помріяти про ідеальне рішення.

абстракції небезпечні

Всім нам подобаються прості інструменти. Складність вбиває. Вона робить наші життя складніше, а криву навчання - більш стрімкою. Програмістам необхідно знати, як речі працюють. Інакше вони відчувають себе невпевнено. Якщо ми працюємо зі складною системою, з'являється великий розрив між «я цим користуюся» і «я знаю, як це працює». Наприклад, такий код приховує складність:

var page = Framework.createPage ({ 'type': 'home', 'visible': true});

Припустимо, що це реальний фреймворк. Під капотом createPage створює новий клас відображення, який завантажує шаблон з home.html. Залежно від значення параметра visible ми вставляємо (чи ні) створений елемент DOM в дерево. А тепер уявіть себе на місці розробника. Ми прочитали в документації, що цей метод створює нову сторінку з заданим шаблоном. Нам невідомі конкретні деталі, тому що це абстракція.

У деяких з фреймворків наших днів навіть не один, а кілька шарів абстракцій. Іноді щоб користуватися фреймворком правильно, нам потрібно знати деталі. Абстрагування, взагалі кажучи, потужний інструмент, це обгортка для функціональності. Воно инкапсулирует конкретні реалізації. Але абстрагування слід використовувати з обережністю, інакше воно може призвести до дій, які неможливо відстежити.

А що якщо ми перепишемо приклад вище ось так:

var page = Framework.createPage (); page .loadTemplate ( 'home.html') .appendToDOM ();

Тепер розробник знає, що відбувається. Створення шаблону і вставка в дерево тепер виробляються різними методами API. Так що програміст може щось зробити між цим викликами і контролює ситуацію.

Візьмемо наприклад Ember.js . Це відмінний фреймворк. З його допомогою ми можемо побудувати односторінкове додаток всього кількома рядками коду. Але все має свою ціну. Він оголошує за лаштунками кілька класів. Наприклад:

App.Router.map (function () {this.resource ( 'posts', function () {this.route ( 'new');});});

Фреймворк створює три маршрути, і за кожним закріплений контролер. Можете використовувати ці класи, можете не використовувати, але вони все одно є. Вони потрібні фреймворку для роботи.

Дуже часто в наших проектах потрібно нестандартна функціональність. Не існує фреймворка на всі випадки життя. Так що, ми стикаємося з завданнями, у яких немає простих рішень. Щоб зробити все правильно, ми повинні розуміти, яким чином все працює. Ухвалення будь-якого іншого рішення нагадує злом фреймворка, а не його використання.

У Backbone.js , Наприклад, всього кілька заздалегідь певних об'єктів. У них міститься базова функціональність, але справжня реалізація залишається за програмістом. Клас DocumentView розширює Backbone.View. Це все. Всього один рівень між нашим кодом і кодом ядра фреймворку.

var DocumentView = Backbone.View.extend ({ 'tagName': 'li', 'events': { 'mouseover .title .date': 'showTooltip', 'click .open': 'render'}, 'render': function () {...}, 'showTooltip': function () {...}});

Особисто я віддаю перевагу фреймворки, в яких немає безлічі рівнів абстракцій, фреймворки, які надають прозорість.

відсутній конструктор

Деякі з фреймворків приймають наші визначення класів, але не створюють конструкторів. Фреймворк сам вирішує, де і коли створити екземпляр. Я був би радий побачити більше фреймворків, що дозволяють нам робити саме так. Ось, наприклад Knockout :

function ViewModel (first, last) {this.firstName = ko.observable (first); this.lastName = ko.observable (last); } Ko.applyBindings (new ViewModel ( "Planet", "Earth"))

Ми оголошуємо модель і самі ж її инициализируем. А ось в AngularJS трохи по-іншому:

function TodoCtrl ($ scope) {$ scope.todos = [{ 'text': 'learn angular', 'done': true}, { 'text': 'build an angular app', 'done': false}]; }

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

Робота з DOM

Що б ми не робили, нам потрібно взаємодіяти з DOM. Те, як ми це робимо, дуже важливо, зазвичай кожна зміна вузлів дерева на сторінці тягне за собою перерахунок розмірів або перемальовування, а це можуть бути вельми дорогі операції. Давайте як приклад розберемо такий клас:

var Framework = { 'el': null, 'setElement': function (el) {this.el = el; return this; }, 'Update': function (list) {var str = '<ul>'; for (var i = 0; i <list.length; i ++) {var li = document.createElement ( 'li'); li.textContent = list [i]; str + = li.outerHTML; } Str + = '</ ul>'; this.el.innerHTML = str; return this; }}

Цей крихітний фреймворк генерує ненумерований список з потрібними даними. Ми передаємо елемент DOM, в якому слід помістити список, і викликаємо функцію update, яка відображає дані на екрані.

Framework .setElement (document.querySelector ( '. Content')) .update ([ 'JavaScript', 'is', 'awesome']);

Ось, що у нас з цього вийшло:

Ось, що у нас з цього вийшло:

Щоб продемонструвати слабку сторону такого підходу ми додамо на сторінку посилання і призначимо на ній обробник події click. Функція знову викличе метод update, але з іншими елементами списку:

document.querySelector ( 'a'). addEventListener ( 'click', function () {Framework.update ([ 'Web', 'is', 'awesome']);});

Ми передаємо майже ті ж самі дані, змінився тільки перший елемент масиву. Але через те, що ми використовуємо innerHTML, перерисовка відбувається після кожного кліка. Браузер не знає, що нам треба поміняти тільки перший рядок. Він перемальовує весь список. Давайте запустимо DevTools браузера Opera і запустимо профілювання. Подивіться на цьому анімованому GIF, що відбувається:

Подивіться на цьому анімованому GIF, що відбувається:

Зауважте, після кожного кліка весь контент перемальовується. Це проблема, особливо, якщо така техніка застосовується в багатьох місцях на сторінці.

Набагато краще запам'ятовувати створені елементи <li> і міняти тільки їх вміст. Таким чином, ми міняємо не весь список цілком, а тільки його дочірні вузли. Перша зміна ми можемо зробити в setElement:

setElement: function (el) {this.list = document.createElement ( 'ul'); el.appendChild (this.list); return this; }

Тепер нам більше не обов'язково зберігати посилання на елемент-контейнер. Досить створити елемент <ul> і один раз його додати в дерево.

Логіка, що поліпшує швидкодію, знаходиться всередині методу update:

'Update': function (list) {for (var i = 0; i <list.length; i ++) {if (! This.rows [i]) {var row = document.createElement ( 'LI'); row.textContent = list [i]; this.rows [i] = row; this.list.appendChild (row); } Else if (this.rows [i] .textContent! == list [i]) {this.rows [i] .textContent = list [i]; }} If (list.length <this.rows.length) {for (var i = list.length; i <this.rows.length; i ++) {if (this.rows [i]! == false) {this .list.removeChild (this.rows [i]); this.rows [i] = false; }}} Return this; }

Перший цикл for проходить по всім переданим рядках і створює при необхідності елементи <li>. Посилання на ці елементи зберігаються в масиві this.rows. А якщо там за певним індексом вже знаходиться елемент, фреймворк лише оновлює по можливості його властивість textContent. Другий цикл видаляє елементи, якщо розмір масиву більше, ніж кількість переданих рядків.

Ось результат:

Браузер перемальовує тільки ту частину, яка змінилася.

Хороша новина: фреймворки начебто React і так вже працюють з DOM правильно. Браузери стають розумнішими і застосовують хитрощі для того, щоб перемальовувати якомога менше. Але все одно, краще тримати це в розумі і перевіряти, як працює обраний вами фреймворк.

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

Обробка подій DOM

Додатки на JavaScript зазвичай взаємодіють з користувачем через події DOM. Елементи на сторінці посилають події, а наш код їх обробляє. Ось уривок коду на Backbone.js, який виконує дію, якщо користувач взаємодіє зі сторінкою:

var Navigation = Backbone.View.extend ({ 'events': { 'click .header.menu': 'toggleMenu'}, 'toggleMenu': function () {// ...}});

Отже, повинен бути елемент, відповідний селектору .header.menu, і коли користувач на ньому клацне, ми повинні показати або приховати меню. Проблема такого підходу в тому, що ми прив'язуємо об'єкт JavaScript до конкретного елементу DOM. Якщо ми захочемо підредагувати розмітку і замінити .menu на .main-menu, нам доведеться поправити і JavaScript. Я вважаю, що контролери повинні бути незалежними, і не слід їх жорстко зчіплювати з DOM.

Визначаючи функції, ми делегуємо завдання класу JavaScript. Якщо ці завдання - обробники подій DOM, є сенс створювати їх з HTML.

Мені подобається, як AngularJS обробляє події.

<a href="#" ng-click="go()"> click me </a>

go - це функція, зареєстрована в нашому контролері. Якщо дотримуватися такого принципу, нам не потрібно замислюватися про селекторах DOM. Ми просто застосовуємо поведінку безпосередньо до вузлів HTML. Такий підхід хороший тим, що він рятує від нудної метушні з DOM.

В цілому, я був би радий, якби така логіка була всередині HTML. Цікаво, що ми витратили купу часу на те, щоб переконати розробників розділяти вміст (HTML) і поведінку (JavaScript), ми відучили їх вбудовувати стилі і скрипти прямо в HTML. Але тепер я бачу, що це може зберегти наш час і зробити наші компоненти більш гнучкими. Зрозуміло, я не маю на увазі щось таке:

<Div onclick = "javascript: App.doSomething (this);"> banner text </ div>

Я говорю про наочних атрибутах, які керують поведінкою елемента. наприклад:

<Div data-component = "slideshow" data-items = "5" data-select = "dispatch: selected"> ... </ div>

Це не повинно виглядати, як програмування на JavaScript в HTML, скоріше це повинно бути схоже на установку конфігурації.

управління залежностями

Управління залежностями - важливе завдання в процесі розробки. Зазвичай ми залежимо від зовнішніх функцій, модулів або бібліотек. Фактично, ми весь час створюємо залежності. Ми не пишемо все в одному методі. Ми розносимо завдання додатки в різні функції, а потім їх з'єднуємо. В ідеалі ми хочемо инкапсулировать логіку в модулі, які ведуть себе як чорні ящики. Вони знають тільки ті деталі, які стосуються їх роботи, і більше нічого.

RequireJS - один з популярних інструментів залежності не будуть задоволені. Ідея полягає в тому, що код обертається в замикання, в яке передаються необхідні модулі:

require ([ 'ajax', 'router'], function (ajax, router) {// ...});

У цьому прикладі функції потрібно два модуля: ajax і router. Магічний метод require читає переданий масив і викликає нашу функцію з потрібними аргументами. Визначення router виглядає приблизно так:

// router.js define ([ 'jquery'], function ($) {return { 'apiMethod': function () {// ...}}});

Зауважте, тут ще одна залежність - jQuery. Ще важлива деталь: ми повинні повернути публічне API нашого модуля. Інакше код, що запитав наш модуль, не зможе отримати доступ до самої функціональності.

AngularJS йде трохи далі і надає нам щось під назвою фабрика. Ми реєструємо там свої залежності, і вони чарівним чином стають доступними в контролерах. наприклад:

myModule.factory ( 'greeter', function ($ window) {return { 'greet': function (text) {$ window.alert (text);}};}); function MyController ($ scope, greeter) {$ scope.sayHello = function () {greeter.greet ( 'Hello World'); }; }

Взагалі кажучи, такий підхід полегшує роботу. Нам не треба використовувати функцій на зразок require для того щоб дістатися до залежності. Все, що потрібно, - надрукувати правильні слова в списку аргументів.

Гаразд, обидва ці способи впровадження залежностей працюють, але кожен з них вимагає свого стилю написання коду. В майбутньому я хотів би побачити фреймворки, в яких це обмеження зняте. Було б значно витонченіше застосовувати метадані при створенні змінних. Зараз мова не дає можливості це зробити. Але було б круто, якби можна було робити так:

var router: <inject: Router>;

Якщо залежність буде знаходитися поряд з визначенням змінної, то ми можемо бути впевнені, що впровадження цієї залежності проводиться, тільки якщо вона потрібна. RequireJS і AngularJS, наприклад, працюють на функціональному рівні. Тобто, може статися так, що ви використовуєте модуль тільки в певних випадках, але його ініціалізація і впровадження відбуватимуться завжди. До того ж, ми можемо визначати залежно тільки в строго визначеному місці. Ми до цього прив'язані.

шаблони

Ми часто користуємося шаблонами. І ми робимо це через необхідність розділяти дані і розмітку HTML. Як же сучасні фреймворки працюють з шаблонами? Ось найпоширеніші підходи:

Шаблон визначений в <script>

<Script type = "text / x-handlebars"> Hello, <strong> </ strong>! </ Script>

Такий підхід часто використовується, тому що шаблони знаходяться в HTML. Це виглядає природно і не позбавлене сенсу, якщо вже в HTML є теги. Браузер не отрісовиваєт вміст елементів <script>, і зім'яло зовнішній вигляд сторінки це не може.

Шаблон завантажується через AJAX

Backbone.View.extend ({ 'template': 'my-view-template', 'render': function () {$ .get ( '/ templates /' + this.template + '.html', function (template) {var html = $ (template) .tmpl ();});}});

Ми поклали свій код в зовнішні файли HTML і уникли використання додаткових тегів <script>. Але тепер нам потрібно більше запитів HTTP, а це не завжди доречно (по крайней мере, поки підтримка HTTP2 не стане ширше).

Шаблон є частиною розмітки - фреймворк отримує його з DOM-дерева. Цей метод спирається на вже згенерований HTML. Нам не потрібно робити додаткових HTTP запитів, створювати нові файли або додаткові елементи <script>.

Шаблон - частина розмітки сторінки

var HelloMessage = React.createClass ({render: function () {// Зверніть увагу: наступний рядок коду // не є коректним JavaScript. return <div> Hello {this.props.name} </ div>;}});

Такий підхід був введений в React, там використовується власний парсер, який перетворює невалидность частина JavaScript в валідний код.

Шаблон - НЕ HTML

Деякі фреймворки взагалі не використовують HTML безпосередньо. Замість цього шаблони зберігаються у вигляді JSON або YAML.

Наостанок про шаблони

Добре, а що далі? Я очікую, що з фреймворком майбутнього ми будемо розглядати дані окремо, а розмітку окремо. Щоб вони не перетиналися. Ми не хочемо мати справу з завантаженням рядків в HTML або з передачею даних в спеціальні функції. Ми хочемо присвоювати значення змінним, а DOM щоб оновлювався сам. Поширене двостороннє зв'язування не повинно бути чимось додатковим, це повинно бути обов'язковою базовою функціональністю.

Взагалі, поведінка AngularJS найближче до бажаного. Він зчитує шаблон з вмісту наданої сторінки, і в ньому реалізовано чарівне двостороннє зв'язування. Втім, воно ще не ідеально. Іноді спостерігається мерехтіння. Це відбувається, коли браузер отрісовиваєт HTML, але завантажувальні механізми AngularJS ще не запустилися. До того ж, в AngularJS застосовується брудна перевірка того, змінилося чи що-небудь. Такий підхід часом дуже витратний. Сподіваюся, скоро в усіх браузерах буде підтримуватися Object.observe , І зв'язування буде краще.

Рано чи пізно кожен розробник стикається з питанням динамічних шаблонів. Напевно, в наших додатках є частини, які з'являються після завантаження. З фреймворком це повинно бути просто. Ми не повинні замислюватися про AJAX-запитах, а API має бути таким, щоб процес виглядав синхронним.

модульність

Мені подобається, коли можливості можна включати і вимикати. A якщо ми чимось не користуємося, навіщо тримати це в коді проекту? Було б добре, якщо у фреймворка був би збирач, який генерує версію з тільки необхідними модулями. Як наприклад YUI , У якого є конфігуратор. Ми вибираємо ті модулі, які хочемо, і отримуємо мініфіцірованний і готовий до використання файл JavaScript.

Навіть так, існують фреймворки, у них є дещо, зване ядром. До того ж до нього ми можемо використовувати пачку плагінів (або модулів). Але ми могли б це поліпшити. Процес вибору потрібних можливостей не повинен включати в себе завантаження файлів. Ми не повинні їх вручну підключати на сторінці. Це якимось чином має бути частиною коду фреймворка.

Крім адекватних можливостей при установці, ідеальне середовище повинна передбачати розширюваність. У нас повинна бути можливість писати власні плагіни і ділитися ними з іншими розробниками. Іншими словами, середовище має бути сприятливою для написання модулів. Чи не вийде створити сильне співтовариство без існування відповідних умов для розробників.

Відкрите API

Велика частина фреймворків надають API до своєї базової функціональності. Але за допомогою цих API можна дістатися тільки до тих частин, які постачальники порахували потрібними для нас. І ось тут можуть знадобитися хакі. Ми хочемо щось отримати, але для цього немає відповідних інструментів. І доводиться йти на хитрощі і рухатися в обхід. Розглянемо такий приклад:

var Framework = function () {var router = new Router (); var factory = new ControllerFactory (); return { 'addRoute': function (path) {var rData = router.resolve (path); var controller = factory.get (rData.controllerType); router.register (path, controller.handler); return controller; }}}; var AboutCtrl = Framework.addRoute ( '/ about');

У такого фреймворка є вбудований маршрутизатор. Ми визначаємо шлях, і контролер инициализируется автоматично. Коли користувач відвідує певний URL, маршрутизатор викликає у конструктора метод handler. Це здорово, але що якщо нам потрібно виконувати невелику функцію JavaScript при збігу URL? З якоїсь причини, ми не хочемо створювати додатковий контролер. З поточним API таке не вийде.

Ми могли б зробити по-іншому, наприклад, ось так:

var Framework = function () {var router = new Router (); var factory = new ControllerFactory (); return { 'createController': function (path) {var rData = router.resolve (path); return factory.get (rData.controllerType); } 'AddRoute': function (path, handler) {router.register (path, handler); }}} Var AboutCtrl = Framework.createController ({ 'type': 'about'}); Framework.addRoute ( '/ about', AboutCtrl.handler);

Зауважте, маршрутизатор НЕ стірчіть назовні. Его не видно, но тепер Ми можемо управляти як Створення контролера, так и реєстрацією шляху в маршрутізаторі. Зрозуміло, запропонованій варіант Підходить для Нашої конкретного завдання. Альо ВІН может віявітіся надмірно складним, тому что контролери тут доводитися створюваті вручну. При розробці API ми керуємося принципом єдиною обов'язки і міркуванням роби щось одне, і роби це добре. Я бачу, як все більше і більше фреймворків децентралізовані свою функціональність. У них складні методи діляться на більш дрібні частини. І це добра ознака, я сподіваюся, в майбутньому більше фреймворків буде так робити.

тестованих

Немає потреби переконувати вас у необхідності писати тести для коду. Справа навіть не тільки в тому, що треба писати тести, а в тому, що треба писати код, який можливо покрити тестами. Іноді це неймовірно складно і займає багато часу. Я переконаний, що якщо ми на щось не напишемо тести, навіть на щось дуже маленьке, то саме в цьому місці в додатку почнуть розмножуватися баги. Це особливо стосується JavaScript на стороні клієнта. Кілька браузерів, кілька операційних систем, нові специфікації, нові можливості і їх поліфілії - так купа причин почати практикувати розробку через тестування.

Є ще дещо, що ми отримаємо від тестів. Ми не тільки переконуємося в тому, що наш фреймворк (додаток) працює сьогодні. Ми переконуємося, що він буде працювати завтра і післязавтра. Якщо є якась нова можливість, яку ми вносимо в код, ми пишемо для неї тести. І дуже важливо, що ми робимо так, щоб ці тести проходили. Але так само важливо, щоб проходили і попередні тести. Саме так ми гарантуємо, що нічого не зламалося.

Я з радістю б побачив більше стандартизованих утиліт і методів для тестування. Мені хотілося б використовувати одну утиліту для тестування всіх фреймворків. Було б ще добре, якби тестування було якось включено в процес розробки. Слід звернути більше уваги на сервіси типу Travis CI . Вони працюють як індикатор не тільки для того програміста, який вносить зміни, але також і для інших контрибуторів.

Я все ще працюю з PHP. Мені доводилося мати справу з фреймворками начебто WordPress. І безліч людей запитувало мене, як я тестую свої додатки: який фреймворк я використовую, як я запускаю тести, чи є у мене взагалі компоненти. Правда в тому, що я нічого не тестую. І все тому у мене немає компонентів. Те ж саме відноситься і деяким фреймворками на JavaScript. Деякі їх частини важко тестувати, бо вони не дробляться на компоненти. Розробникам слід подумати і в цьому напрямку. Так, вони надають нам розумний, витончений і робочий код. Але код повинен бути ще й тестується.

документація

Я впевнений, що без гарної документації будь-який проект рано чи пізно загнеться. Щотижня виходить купа фреймворків і бібліотек. Документація - це перше, що бачить розробник. Ніхто не хоче витрачати години на те, що дізнатися, що робить певна утиліта, і які у неї можливості. Недостатньо простого перерахування основної функціональності. Особливо для великого фреймворка.

Я б розділив хорошу документацію на три частини:

  • Що я можу зробити - документація повинна вчити користувача і повинна робити це правильно. Неважливо, наскільки крутий або потужний у нас фреймворк, він має потребу в поясненні. Хтось вважає за краще дивитися відео, хтось читати статті. У будь-якому випадку, розробнику потрібно показати все, починаючи з самих основ і закінчуючи складними частинами фреймворка.
  • Документація API - це зазвичай є скрізь. Повний список всіх публічних методів API, того, які у них параметри і що вони повертають. Може бути, приклади використання.
  • Як це працює - зазвичай цього розділу в документациях немає. Добре, якби хто-небудь роз'яснив структуру фреймворка, навіть проста схема базової функціональності і її взаємозв'язку вже б допомогла. Це зробило б код прозорим. Це допомогло б тим розробникам, які хочуть внести свої зміни.

Висновок

Майбутнє, звичайно, важко передбачити. Але зате ми можемо про нього помріяти! Важливо говорити про те, що ми очікуємо і що ми хочемо від фреймворків на JavaScript! Якщо у вас є зауваження, пропозиції або ви хочете поділиться своїми думками, пишіть в Твітер з хештегом #jsframeworks .

Як же сучасні фреймворки працюють з шаблонами?
A якщо ми чимось не користуємося, навіщо тримати це в коді проекту?
Це здорово, але що якщо нам потрібно виконувати невелику функцію JavaScript при збігу URL?