[Перевод] В поисках идеального фреймворка для JavaScript

В наше время для разработки фронтенда существует много фреймворков и библиотек. Есть хорошие, есть не очень. Часто нам нравится только какая-то концепция, модуль или синтакс. Универсальных инструментов не существует. В статье я описываю фреймворк будущего — такой, которого ещё нет. Я собрал достоинства и недостатки известных фреймворков и мечтаю об идеальном решении.Абстракция опаснаВсем нравится простота. Сложность убивает. Она усложняет работу и приводит к крутой кривой обучения. Программистом нужно понимать, что как работает — иначе они чувствуют себя неуверенно. При работе со сложной системой есть большое расстояние между «я её использую» и «я знаю, как это работает». К примеру, следующий код прячет сложность: var page = Framework.createPage ({ 'type': 'home', 'visible': true }); Допустим, это реальный фреймворк. createPage где-то создаёт новый класс Вида, загружающий html-шаблон home. Основываясь на параметре 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 («Планета», «Земля»)) Мы определяем модель и инициализируем её. В AngularJS всё немного по-другому:

function TodoCtrl ($scope) { $scope.todos = [ { 'text': 'Учи angular', 'done': true }, { 'text': 'Делай приложение на angular', 'done': false } ]; } Мы определяем класс, но не запускаем его. Мы просто говорим, что это — наш контроллер, а фреймворк решает, как с ним работать. Это может сбивать с толку, т.к. мы теряем ключевые точки в коде, которые позволяют ориентироваться в работе приложения.

Манипуляции DOM Нам в любом случае необходимо взаимодействовать с DOM. И нам надо точно знать, как это происходит — обычно, каждое действие с узлами страницы приводит к её перерисовке, что может быть довольно затратным. К примеру, рассмотрим следующий класс: var Framework = { 'el': null, 'setElement': function (el) { this.el = el; return this; }, 'update': function (list) { var str = '

    '; for (var i = 0; i < list.length; i++) { var li = document.createElement('li'); li.textContent = list[i]; str += li.outerHTML; } str += '
'; this.el.innerHTML = str; return this; } } Этот фреймворк создаёт ненумерованный список из заданных данных. Мы отправляем элемент DOM, в котором будет содержаться список, и вызываем update, которая показывает данные на экране.

Framework .setElement (document.querySelector ('.content')) .update (['JavaScript', 'крутой', 'язык']); Результат следующий:

image

Покажем, почему это плохо. Добавим ссылку на страницу и навесим отслеживание событий. Функция вызовет update уже с другими параметрами:

document.querySelector ('a').addEventListener ('click', function () { Framework.update (['Веб', 'крутая', 'штука']); }); Мы отправляем очень похожие данные и меняем только первый элемент массива. Но из-за использования innerHTML каждый раз вызывается перерисовка всего списка. Давайте посмотрим на это через Opera«s DevTools.

image

После каждого клика перерисовывается всё содержимое. Это проблема.

Было бы лучше, если б мы работали только с узлами

  • . Тогда мы будем менять не весь список, а только его потомков. Первое, что нужно поменять — это setElement:

    setElement: function (el) { this.list = document.createElement ('ul'); el.appendChild (this.list); return this; } Теперь мы не будем ссылаться на внешний элемент. Нужно лишь создать

      и один раз его добавить.

      Логика, улучшающая быстродействие, находится в методе 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 проходит данные и создаёт элементы

    • . this.rows содержит созданные элементы. Если по определённому индексу есть узел, фреймворк обновляет его свойство textContent. Цикл в конце удаляет узлы, если в полученном массиве элементов меньше, чем в текущем.

      Результат:

      image

      Браузер перерисовывает только изменившуюся часть.

      Фреймворки типа React корректно работают с манипуляциями DOM. Браузеры становятся умнее и стараются уменьшить количество перерисовок. Но всегда неплохо иметь это в виду и проверять работу фреймворка.

      Надеюсь, в будущем нам не придётся думать о таких вещах.

      Обработка событий DOM Приложения JavaScript общаются с пользователями через события DOM. Элементы страницы отправляют сообщения, а код их обрабатывает. Вот кусок кода Backbone.js, обрабатывающего взаимодействие пользователя со страницей: var Navigation = Backbone.View.extend ({ 'events': { 'click .header.menu': 'toggleMenu' }, 'toggleMenu': function () { // … } }); Должен быть элемент, соответствующий селектору .header.menu, и когда пользователь по нему кликает, мы переключаем меню. Проблема в том, что мы привязываем объект к определённому элементу DOM. Если мы поменяем код и переименуем .menu. в .main-menu, нам придётся поменять JS-код. Я считаю, что контроллеры должны быть независимыми, и их надо отвязать от DOM.

      Определяя функции, мы передаём таски классам JS. Если эти таски — хэндлеры событий DOM, то имеет смысл включить их в HTML.

      Мне нравится обработка событий в AngularJS:

      жмакай меня go — функция, зарегистрированная в контроллере. И тогда нам не надо думать про селекторы DOM. Мы просто назначаем поведение узлам HTML. И пропускаем скучный этап взаимодействия с DOM.

      Хотелось бы видеть такую логику внутри HTML. Мы годами приучали разработчиков к разделению содержимого (HMTL) и поведения (JS). А теперь я вижу, что их объединение могло бы сэкономить нам массу времени и добавить гибкости. Но я не имею в виду код вроде:

      и тут текст
      Я имею в виду описательные атрибуты, управляющие поведением элемента. К примеру:

      Это должно быть похоже не на включение кода в HTML, а на указание настроек.

      Управление зависимостями При разработке очень важно правильно управлять зависимости. Мы обычно полагаемся на внешние библиотеки и функции. И постоянно сами создаём зависимости — мы ведь не пишем всё в один метод. Мы разбиваем приложение на функции и связываем их. В идеале нам надо инкапсулировать логику в модули, которые работают как «чёрные ящики». Они знают только то, что им нужно для их работы.RequireJS — популярный инструмент для работы с зависимостями. Идея в том, чтобы обернуть код в замыкание, принимающее необходимые нам модули:

      require (['ajax', 'router'], function (ajax, router) { // … }); В примере нашей функции нужны модули ajax и router. Волшебный метод require обрабатывает массив и вызывает функцию с нужными аргументами. Определение router выглядит так:

      // router.js define (['jquery'], function ($) { return { 'apiMethod': function () { // … } } }); Тут у нас есть ещё одна зависимость — jQuery. Важно упомянуть, что нам необходимо вернуть публичное API нашего модуля — иначе код, который включает наш модуль, не сможет получить доступ к нужной функциональности.

      AngularJS идёт ещё дальше и предоставляет нечто под названием factory (фабрика). Мы регистрируем там зависимости, и они волшебным образом становятся доступны в контроллерах:

      myModule.factory ('greeter', function ($window) { return { 'greet': function (text) { alert (text); } }; }); function MyController ($scope, greeter) { $scope.sayHello = function () { greeter.greet ('Всем привет!'); }; } Обычно это упрощает работу — нам не надо использовать функцию require для доступа к зависимостям. Надо только вписать нужные слова в список аргументов.

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

      var router: ; Это бы означало, что мы сделаем инъекцию только по необходимости. RequireJS и AngularJS работают с функциями, и вы можете использовать модуль достаточно редко, но инициализация будет проходить каждый раз, и зависимости необходимо определять в жёстко заданных местах.

      Шаблоны Шаблоны используются для разделения данных и разметки HTML. Как это делается на сегодняшний день? Вот самые популярные подходы.Шаблон определяется в  Шаблон сидит в HTML, выглядит естественно, браузер не рендерит содержимое тега в