Old Skull — фронтенд-фреймворк из альтернативной вселенной

image-loader.svg

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

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

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


Как было?

image-loader.svg

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

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

Такие шаблоны могли быть реализованы в виде обычных 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

image-loader.svg

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

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

class TaskView extends OsfModelView {
  getHTML() {
    const task = this.model.attrs;
    return `
      

${ task.name }

Completed:

`; } 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 {
  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 `
      
`; } contentRegion = new OsfRegion(this, '.content'); async afterInit() { await this.contentRegion.show(new TaskPresenter()); } }

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


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

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

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

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

image-loader.svg

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

image-loader.svg

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

image-loader.svg

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

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

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


Ссылки:


© Habrahabr.ru