Около десяти лет назад сообщество веб-разработчиков впервые начало обсуждать концепцию "Single-Page Application" и искать способы ее реализации. К тому моменту разработка графических интерфейсов уже не являлась чем-то новым и поэтому многие вещи заимствовались у существующих решений и немного адаптировались под специфику браузеров.


Наиболее успешным результатом подобной работы оказался Backbone.js — объектно-ориентированный MVC-фреймворк, который в свое время использовался в BitBucket, Basecamp, Stripe, Airbnb и Trello. Со временем он был полностью вытеснен следующим поколением фреймворков, но...


Что если бы этого не случилось? Как бы тогда выглядела современная разработка веб-интерфейсов?


Как было?



Для начала давайте переместимся назад во времени и посмотрим на то, как именно раньше осуществлялась разработка SPA.


Сначала наши "компоненты" описывалась с помощью текстовых шаблонов:


<script id="task-template" type="text/template">
  <h2 class="task__name">
    <%- name %>
  </h2>
  <p>
    Completed:
    <input class="task__status" type="checkbox" <%= isCompleted ? 'checked' : '' %>>
  </p>
</script>

Такие шаблоны могли быть реализованы в виде обычных JavaScript-строк, но этот подход не пользовался особой популярностью из-за сложности работы с многострочным текстом (template strings на тот момент не существовало).


Затем эти шаблоны использовались в самих "компонентах", которые здесь и далее мы будем называть View:


// Создаем класс TaskView
var TaskView = Backbone.View.extend({
  // Указываем HTML-тег и класс корневого элемента
  tagName: 'div',
  className: 'task',
  // Создаем функцию, которая будет принимать бизнес-данные и возвращать HTML-разметку содержимого View
  // Здесь мы используем jQuery для получения текста шаблона и Underscore для шаблонизации
  template: _.template($('#task-template').html()),
  // Определяем как создается содержимое корневого элемента
  render: function() {
    // Модель - это класс с нашими данными и функциями для работы с ними
    var data = this.model.toJSON();
    var html = this.template(data);
    // Вставляем HTML в корневой элемент
    this.$el.html(html);
  },
});

Для обработки пользовательского ввода указывался CSS-селектор источника события, название прослушиваемого события и название (sic!) функции-обработчика:


var TaskView = Backbone.View.extend({
  // ...
  events: {
    'click .task__status': 'onStateToggle',
  },
  onStateToggle: function() {
    this.trigger(TOGGLE_TASK_STATE);
  },
});

Чтобы отреагировать на изменение отображаемых данных следовало подписаться на соответствующее событие модели и указать функцию-обработчик:


var TaskView = Backbone.View.extend({
  // ...
  initialize: function() {
    this.listenTo(this.model, 'change', this.render);
  },
});

Обратите внимание, здесь используется "ленивый" обработчик события — любое изменение данных приводит к полному пересозданию элементов. Лучшей практикой считается прослушивание изменения конкретного атрибута модели и ручное обновление его отображения в DOM:


var TaskView = Backbone.View.extend({
  // ...
  initialize: function() {
    this.listenTo(this.model, 'change:isCompleted', this.handleStatusChange);
  },
  handleStatusChange: function() {
    var isCompleted = this.model.get('isCompleted');
    this.$el.find('.task__status').prop('checked', isCompleted);
  }, 
});

Что было плохого?


Безусловно, Backbone не был идеальным решением и его использование всегда было связано с проблемами и трудностями, причинами которых были:


1. Пробелы в архитектуре


На самом деле Backbone был не полноценным самостоятельным решением, а скорее фреймворком для создания других фреймворков. К примеру, в нем полностью отсутствовали контроллеры (С из MVC, посредник между данными и их отображением), отсутствовала стандартная реализация функции "View.render()", в нем не было стандартных способов отображения списков данных и вложения одних View внутрь других.


Необходимость самостоятельного поиска решений для всех вышеперечисленных задач серьезно затрудняла использование фреймворка рядовыми специалистами и приводило к тому, что каждое приложение на Backbone разрабатывалось в своем собственном уникальном стиле.


2. Внешние зависимости


Для использования Backbone требовалось подключение трех сторонних библиотек:


  • jQuery для работы с DOM и выполнения запросов к серверу
  • Underscore для работы с массивами и объектами
  • Продвинутый шаблонизатор (Handlebars и ему подобные)

Это существенно увеличивало размер приложения и ставило фреймворк в сильную зависимость от сторонних проектов.


3. Developer Experience (?)


Главным преимуществом следующего поколения фреймворков являлось существенное упрощение и ускорение процесса разработки. Как уже было продемонстрировано выше, обновление данных во View ранее требовало написания большого количества кода, а в новой реактивной модели разработки в этом не было никакой необходимости — достаточно было единожды описать что и где должно отображаться, а все дальнейшие обновления происходили автоматически.


Но это преимущество совсем не так однозначно если мы посмотрим на ситуацию с точки зрения конечного пользователя. Дело в том, что эти автоматические обновления даются нам не бесплатно — они увеличивают потребление RAM (хранение VDOM) и CPU (процедура сравнения VDOM), что в конечном итоге приводит к замедлению реакции интерфейса и увеличению расхода батареи мобильных устройств.


Так что в данном случае мы имеем некоторое противоречие: с одной стороны разработка ускоряется и удешевляется, но с другой — качество конечного продукта ухудшается.


Что было хорошего?


Стоит коротко упомянуть и те вещи, которые изначально были сделаны правильно:


1. Архитектура


Разработчикам навязывалось использование лучших практик, они вынуждены были соблюдать принцип разграничения ответственности и использовать событийную модель взаимодействия.


2. Минимальная абстракция


Разработчики имели прямой доступ к DOM, что позволяло использовать любые нативные библиотеки (слайдеры, попапы и т.п.) без применения специфичных для фреймворка оберток.


3. Кодовая база


Исходный код фреймворка был предельно прост и краток, в нем можно было полностью разобраться буквально за пару вечеров.


4. Нативность


Фреймворк не расширял стандарты платформы (HTML, CSS, JS) и не изобретал новых, вследствие чего его использование не требовало ни модификации инструментов разработки, ни добавления дополнительных шагов сборки.


5. Использование ООП


Да, оно имеет спорную репутацию, но ООП отлично ложится на специфику разработки GUI, где оно давно и успешно применяется (Qt, GTK, да и сам DOM), и тем более оно более актуально в контексте JavaScript, в котором нет ни иммутабельности, ни структур данных, не требующих полного перевыделения памяти при каждом изменении.


Как могло бы быть?


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


Чтобы проверить эту гипотезу мы создадим функциональный прототип этакого "Backbone 2.0", и начнем мы с того, что сформулируем требования к нему. Чего бы мы хотели и ожидали от фреймворка в 2021-м году? Ответ на этот вопрос уже дан выше:


1. Полноценной реализации архитектуры и всех ключевых функций


Решение должно справляться со своей основной задачей сразу же из коробки. Кастомизация должна быть возможна, но не должна препятствовать быстрому началу работы.


2. Отсутствия внешних зависимостей


Нужны ли нам сейчас jQuery, Underscore и шаблонизаторы? Нет:


  • jQuery может быть полностью заменен современным API браузеров (querySelector, fetch и прочее)
  • Основные функции Underscore уже реализованы в самом языке (find, filter, concat, etc.)
  • Шаблонизаторы теоретически могут быть заменены с помощью template strings

3. Адекватного Developer Experience


Выше мы уже определили, что DX несколько конфликтует с UX, и наша задача — сделать так, чтобы разработчики могли самостоятельно решать что из этого им важнее. Нам следует предоставить возможность выбора:


  • Нужен DX: выполняем полный ре-рендеринг при обновлении данных
  • Нужен UX: пишем все обработчики вручную
  • Нужен компромисс: используем во "View.render()" либо React, либо его легковесные альтернативы

Да, ничто не мешает нам подключить и использовать другой рендерер внутри View. В этом и заключается одна из прелестей Backbone и MVC — они определяют области ответственности и их интерфейсы, но не навязывают их реализацию.


Ну и если мы говорим про DX вообще, то обеспечить его мы можем предоставив продуманный API, обширную документацию (API, комментарии к коду, обучающие материалы) и используя в разработке более современные технологии и подходы, где нам сильно мог бы помочь TypeScript с его более полноценной реализацией ООП и относительной типобезопасностью.


NB: Использование TypeScript хоть и противоречит нативности, но его использование решает намного больше проблем, чем создает.


Собственно, Old Skull



Итак, берем Backbone, выкидываем из него сомнительное и неудачное, заполняем возникшие и уже присутствовавшие пробелы, выполняем глобальный рефакторинг и… получаем Old Skull Framework.


Для создания View теперь достаточно просто указать HTML и описать логику обновления. Больше никакой головной боли с шаблонизаторами и ручным инстанцированием элементов:


class TaskView extends OsfModelView<TaskModel> {
  getHTML() {
    const task = this.model.attrs;
    return `
      <div class="task">
        <h2 class="task__name">
          ${ task.name }
        </h2>
        <p>
          Completed:
          <input class="task__status" type="checkbox" ${task.isCompleted ? 'checked' : ''}>
        </p>
      </div>
    `;
  }
  domEvents = [
    {
      el: '.task__status',
      on: 'click',
      call: this.onStateToggle.bind(this),
    },
  ];
  modelEvents = [
    {
      on: 'change isCompleted',
      call: this.handleStatusChange.bind(this),
    },
  ];
  onStateToggle() {
    // ...
  }
  handleStatusChange() {
    // ...
  }
}

Взаимодействие между данными и их отображением теперь единообразно регулируется с помощью Presenter:


class TaskPresenter extends OsfPresenter<TaskModel, TaskView> {
  model = new TaskModel({
    name: 'foobar',
    isCompleted: false,  
  });
  view = new TaskView(this.model);
  viewEvents = [
    {
      on: TOGGLE_TASK_STATE,
      call: this.handleViewStatusChange.bind(this),
    },
  ];
  handleViewStatusChange() {
    this.model.toggleState();
  }
}

Вложение одного View внутрь другого больше не является проблемой:


class LayoutView extends OsfView {
  getHTML() {
    return `
      <div class="page">
        <div class="content"></div>
      </div>
    `;
  }
  contentRegion = new OsfRegion(this, '.content');
  async afterInit() {
    await this.contentRegion.show(new TaskPresenter());
  }
}

В аналогичном стиле реализовано и всё остальное:


  • Отображение списка элементов: CollectionView
  • Работа с данными: Model и Collection
  • Создание и инициализация приложения: Application
  • Получение ссылок на DOM-элементы: Reference

Итоговый размер получившегося фреймворка удивляет и даже заставляет усомниться в корректности замеров:


Package Minified + Gzipped
@angular/core@12.2.9 89.9 kB
react-dom@17.0.2 39.4 kB
oldskull@2.0.0 3.8 kB

И самое интересное — это производительность. Результаты js-framework-benchmark показывают, что Old Skull существенно обгоняет по производительности как Angular, так и React:


Длительность (мс):



Метрики запуска:



Выделение памяти (Мбайт):



NB: Скриншоты сделаны 12-го октября 2021-го года с официальных результатов


В результате мы видим, что развитие идей, заложенных в Backbone.js, действительно приводит нас к ощутимо более доступным и отзывчивым веб-интерфейсам, которые к тому же реализуются таким образом, что разница между традиционными языками программирования и JavaScript сводится к минимуму.


Что вы думаете обо всем этом? Имеют ли ООП и MVC место во фронтенд-разработке? Насколько DX важнее UX? И… не возникает ли у вас ощущение, что мир фронтенд-разработки где-то повернул не туда?




Ссылки:


Комментарии (13)


  1. kucheruk
    14.10.2021 12:27
    +4

    Результаты бенчмарков вдохновляют, без капли иронии.

    Расстраивает необходимость держать html в строках.

    Это не то чтобы блокер, но для работы с html написана масса инструментов, которые в таком случае использовать не выйдет.

    Насколько DX важнее UX?

    Когда речь идёт про отзывчивость интерфейса и скорость загрузки, DX хорошо бы отодвигать на второй план. Не все готовы, к сожалению.


    1. Raspy
      14.10.2021 13:29
      +3

      Так-то современные IDE понимают что в темплейт-стринге находится html и осуществляют подсветку синтаксиса, автодополнения и прочие вещи.


      1. faiwer
        18.10.2021 13:58

        А какие, если не секрет? WebStorm?


        vsCode


        1. JustDont
          18.10.2021 17:42

          Для vscode есть extensions (в основном написанные для Polymer), которые что-то такое делают, только конечно не прямо вот "самостоятельно", а с некоторыми движениями руками (типа там, писать именно через tagged template literal — html`` и тому подобное).


        1. Raspy
          20.10.2021 12:34
          +1

          Я пользуюсь Intellij IDEA. Есть как подсветка локальных переменных, так и всех тегов. Если навестись на имя css класса, то покажет в каком css определён этот класс.


          1. faiwer
            20.10.2021 12:42

            Good. А если не убирать пустые пробелы и перенос строки перед <div class тоже работает?


            Есть как подсветка локальных переменных

            Тут как раз всё просто — это же интерполяция. Так любой редактор сможет. А вот воспринять автоматически это как HTML без бубнов, возможно, только Idea.


            1. Raspy
              21.10.2021 14:53
              +1

              Работает как угодно. Очень рекомендую попробовать. У идеи есть EAP версия, это полноценная бесплатная версия, которую можно использовать для любых целей. Единственное что её нужно постоянно обновлять на самую свежую версию, так как вас используют как "бета-тестера".


    1. JustDont
      14.10.2021 14:19
      +4

      Результаты бенчмарков вдохновляют, без капли иронии.

      Были времена, когда svelte (тогда v2) хвалилась результатами бенчмарков на уровне ваниллы.
      А потом туда добавили удобства работы, разобрались с большинством сложных нетривиальных случаев компоновки компонент, починили баги, и теперь её вообще достаточно легко можно брать в кровавый энтерпрайз. Но бенчмарками уже не хвалятся сильно. Догадайтесь, почему.


  1. atomic1989
    14.10.2021 12:32

    Как по мне, все решается сложностью проекта. Учитывая опыт на Backbone, для крупных проектов я бы не использовал. Для очень простых проектов возможно стоит рассмотреть. Вопрос только: svelte или old skull). Для чего-то побольше, все таки лучше react, vue. А для жирненького angular. Они, как по мне, более удобные. old skull не дает нам ничего особого, чтобы побить лидеров.


  1. halfcupgreentea
    14.10.2021 13:46
    +2

    Как будто с автоматической коробки пересел за механику: клево конечно, но на каждый день не надо, спасибо.

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

    По моему, если задаваться целью ускорения современного фронтенда и уменьшения объема кода, то лучше копать в сторону AOT компиляции, как уже делают Svelte и Angular. В реакте похожий проект отложили до лучших времен. Но у них другая киллер фича в приортете: параллелизация отрисовки с помощью коопертивного планировщика (это если почти на русский перевести :) )


  1. justboris
    15.10.2021 00:54
    +2

    В статье пропустили самый важный момент успеха современных фреймворков и неуспеха Backbone – возможность создания переиспользуемых компонентов. Берем и пишем:

    <div class="something">
      <Button>click me</Button>
    </div>

    В любом современном фреймворке можно вставить в HTML кастомный элемент, который отрендерит свою разметку и инициализирует свою логику. В Backbone/Marionette так не выйдет – извольте отрендерить контейнер (регион), а потом вручную в него примонтировать компонент.

    Видели хоть одну библиотеку UI-компонентов на Backbone? То-то и оно, что на нём такие вещи невозможны в принципе. А на Angular/React/Svelte/Vue/etc – запросто.


  1. TheShock
    16.10.2021 03:30
    +3

    Вы неправильно пишите аббревиатуры в camelCase стиле. Проверить очень просто. Подумайте, как бы вы ее писали в underscore стиле: get_h_t_m_l или get_html?

    Выбираем, конечно, второй вариант и каждую букву после подчеркивания заменяем на большую. Получается getHtml

    Есть ещё пример - подумайте какой вариант читабельнее: XMLHTTPRequest или XmlHttpRequest ?


  1. DarthVictor
    17.10.2021 23:38

    И самое интересное — это производительность. Результаты js-framework-benchmark показывают, что Old Skull существенно обгоняет по производительности как Angular, так и React:

    Как Angular, так и React всё что новее будет обгонять по производительности. Это связано с банальной обратной совместимостью. Замените React хотя бы Preact'ом, которому не нужно оборачивать любое событие синтетическим (и который при этом совместим с доброй половиной экосистемы React и имеет тот же API) и разница станет уже существенно менее заметной. Равно как и разница с VueJS, который чуть новее React'а. Ну, а относительно новые SolidJS и Svelte на бенчмарках просто быстрее. Потому что у одного нормальная реактивность на атомах без VDOM, а другого анализ зависимостей в compile time.

    Сравнение с упомянутыми фреймворками