Фреймворк-независимое браузерное SPA +7


1. Но... зачем?

  1. Существует огромное количество фреймворков для разработки SPA (Single Page Application).

  2. Существует огромное количество документации, иллюстрирующей как создавать приложение на базе конкретного фреймворка.

  3. Однако всякая подобная документация ставит фреймворк во главу угла. Тем самым превращая фреймворк из детали реализации в определяющий фактор. Таким образом значительная часть кода пишется не для удовлетворения нужд бизнеса а для удовлетворения нужд фреймворка.

Учитывая насколько hype-driven является разработка софта в наше время, можно быть уверенным в том что через несколько лет будут существовать новые модные фреймворки для фронтенд разработки. В момент когда фреймворк на базе которого построено приложение выходит из моды — вы вынуждены либо поддерживать устаревшую (legacy) кодовую базу либо стартовать процесс перевода приложения на новый фреймворк.

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

Данная статья является примером построения SPA с использованием высокоуровневых принципов дизайна архитектуры. При этом конкретные библиотеки и фреймворки выбираются для удовлетворения ответственностей, определённых желаемой архитектурой.

2. Архитектурные цели и ограничения

Цели:

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

  2. Стимулируется разделение ответственностей (separation of concerns) и следовательно модульность кода так что:

    • Модули легко поддаются тестированию

    • Интеграции с внешними сервисами (boundaries) а также грязные хаки и воркэраунды вынесены в отдельные модули и не протянуты через несколько различных файлов. Таким образом смена реализации интеграции с сервисом или отказ от хака становится реалистичной задачей а не «долгосрочным рефакторингом»

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

  4. Позволяет реализовать слабую связность между модулями. Как следствие изменение реализации модуля (или его замена) не должна приводить к изменению кода, использующего данный модуль.

  5. Механики взаимодействия модулей не приводят к недопустимым проблемам с производительностью.

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

Ограничения:

Приложение должно работать в браузере. Следовательно оно должно быть написано с использованием (или скомпилировано в) HTML+CSS для определения статического интерфейса и JavaScript для добавления динамического поведения.

3. Ограничим тему данной статьи

Существует большое количество архитектурных подходов к структурированию кода. Наиболее распространенные на данный момент: слоеная (layered), луковичная (onion) и шестигранная (hexagonal). Беглое сравнение было дано в моей предыдущей статье.

Данная статья ограничивается слоем представления в терминологии слоеной/луковичной архитектур поскольку большинство SPA занимается исключительно отображением данных. Таким образом слои домена (domain) и приложения (application) могут быть проигнорированы. Как следствие, наиболее естественный способ понять назначение такого приложения — получить обзорное представление о слое представления.

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

Интересно отметить что в случае отсутствия вышеупомянутых слоев приложение напоминает классическую шестигранную структуру (также называемую Ports and Adapters) в которой представление является приложением. Взгляните на интеграцию с localStorage в TodoMVC примере созданном в качестве иллюстрации к данной статье (папка boundaries/local-storage).

4. Структура файлов. Как заставить SPA кричать?

Будем исходить из терминологии дяди Боба.

Рассмотрим типичный онлайн магазин. Приблизительно так он мог бы быть нарисован на салфетке владельцем бизнеса:

Рисунок 1: типичный онлайн магазин, нарисованный на салфетке

Каким может быть наиболее кричащий способ структурировать кодовую базу? На рисунке 2 все страницы отражены как папки.

Рисунок 2: структура папок верхнего уровня, отражающая страницы определённые на рисунке 1

Заметим что мы добавили папку ‘shared’ как место где будут определены общие UI блоки, такие как шаблон, панель навигации, корзина.

Наши страницы построены из логических (и видимых) частей. Пока что назовем их ‘блоками’ и положим в папку с именем ‘parts’. Посмотрим что получилось (рисунок 3).

Рисунок 3: размещение вложенных блоков внутри подпапки ‘parts’

Как видно, вложенность выглядит отвратительно уже для второго уровня для страницы ’goods catalogue’. Путь ‘goods-catalogue/parts/goods-list/parts/good-details.js’ уже на границе адекватной длины пути к файлу. При том что в реальных приложениях два уровня вложенности — далеко не предел.

Давайте избавимся от папок «parts» в файловой структуре. Посмотрим на рисунок 4.

Рисунок 4: вложенные блоки вынесены из папок ‘parts’

Теперь внутри пути ‘goods-catalogue/goods-list’ находится три файла. goods-list.js (родительский) — расположен между файлами, определяющими вложенные в него блоки. В реальных проектах, учитывая кол-во разнородных файлов (js, html, css) это приводит к невозможности разделить файлы, определяющие текущий блок и файлы, отвечающими за вложенные в него блоки.

Решение:

  1. Если конкретный блок определяется несколькими файлам — создаем для него папку.

    • goods-list является блоком и состоит из более чем одного файла, потому для него создана папка.

    • filters является блоком состоящим из одного файла, потому для него не создана отдельная папка.

  2. Если конкретный блок (неважно из одного файла или из нескольких) является вложенным блоком — добавим к названию файла префикс «. Таким образом все вложенные блоки будут подняты к верху папки в файловом обозревателе.

    • _goods-list folder является вложенным блоком относительно goods-catalogue соответственно к названию папки добавлен префикс.

    • goods-list.js является частью определения блока _goods-list соответственно префикс не добавлен.

    • _good-details.js является вложенным блоком относительно _goods-list соответственно префикс добавлен.

Рисунок 5: использование префикса «_» для разделения вложенных блоков от их родителей

Готово! Теперь открывая папку с блоком мы можем сразу же увидеть и открыть основной файл, определяющий данный блок. После чего при необходимости перейти к вложенному блоку. Обратите внимание что папка pages была переименована в components на рисунке 5. Так сделано поскольку страницы и блоки логически являются разными вещами но в терминологии HTML и то и другое может бы представлено как component. С этого момента папка components является основной папкой нашего приложения, «домом» для слоя представления.

5. Язык разработки. JavaScript?

Единственный язык который может быть выполнен в браузере это JavaScript. Существует множество статей посвященных его несуразности. Вы можете посмеяться о нем (тайм код 1-20), но это только веселая часть...

Важнее то, что новые возможности постоянно добавляются к языку. Спецификация обновляется каждый год. Новый фичи проходят 4-этапный процесс ревью перед тем как попасть в спецификацию. Однако зачастую, они реализуются браузерами ещё до прохождения через все 4 этапа. И довольно часто сообщество и авторы библиотек начинают использовать определенные фичи до того как они попадают в спецификацию. К примеру, декораторы стали широко применяться в 2015 году, но до сих пор не являются частью спецификации. С другой стороны, зачастую бизнес требует работоспособности приложения в устаревших браузерах, которые априори не поддерживают новых языковых возможностей.

Потому даже при использовании чистого JavaScript разработчик вынужден использовать транспилятор (babel) с тем чтобы получить JavaScript, совместимый с браузерами из «современного и модного» JavaScript. Поскольку использование транспилятора и соответствующее замедление сборки приложения неизбежно — нет причин игнорировать другие, более предсказуемые и более функциональные языки программирования.

Глубокий анализ возможных опций лежит вне границ данной статьи, но мой персональный выбор — TypeScript поскольку он:

  • Обеспечивает проверку типов на этапе компиляции

  • Будучи над-множеством JavaScript, может выполнять JavaScript код без дополнительного интеграционного кода

  • Определения типов (typings) могут быть добавлены поверх существующего JavaScript кода без его изменения. Благодаря простоте этой возможности, большинство существующих npm пакетов уже покрыты тайпингами. Таким образом вы можете использовать эти пакеты так, как будто бы они являются TypeScript пакетами. Соответственно их использование также является типо-безопасным.

Хинт: рекомендую посмотреть в сторону asm.jsblazor и elm если вы заинтересованы в других опциях

6. Требования к дизайну приложения

Давайте вспомним ограничения, накладываемые браузерами: HTML, CSS, JavaScript. Также вспомним структуру файлов, определенную в разделе 4: дерево директорий, отражающее дерево визуальных элементов.

Таким образом первой целью [6.1] будет возможность определения компонентов средствами HTML и CSS и их последующее переиспользование другими компонентами.

Существенный недостаток чистого HTML состоит в том что он не типизирован. Существует достаточное количество движков шаблонизации, таких как underscore.jshandlebars.js. Однако все они принимают на вход строки, что ограничивает нас в проверке корректности используемых в шаблоне данных на этапе компиляции приложения.

Таким образом второй целью [6.2] является возможность определить TypeScript интерфейсы отражающие все свойства, используемые в шаблоне (компоненте). После чего на этапе компиляции выбросить исключение в случае если в разметке компонента происходит обращение к неопределенному свойству.

Каждый UI элемент может выглядеть по разному в зависимости от переданных ему данных. HTML элементы принимают данные в виде HTML атрибутов. Этого достаточно для статической разметки. Для динамически изменяемой разметки нам необходимы некоторые хранилища для данных. В этих хранилищах данные будут изменяться в зависимости от действий пользователя на странице. В то же время, мы не должны потерять возможность передавать данные в компоненты в виде атрибутов.

Таким образом третьей целью [6.3] является возможность компонентов принимать данные из атрибутов и из хранилищ одновременно. Компоненты должны быть перерисованы при изменении любой части принимаемых данных.

Четвертой целью [6.4] станет определение требований к таким хранилищам:

  • Должна существовать возможность использовать хранилище несколькими компонентами, каждый из которых отображает лишь часть общего набора данных.

  • Должна существовать возможность создавать хранилища вместе с конкретным компонентом. Это необходимо для случая когда различные экземпляры компонента автономны друг от друга и должны иметь различные наборы данных.

  • Хранилища должны иметь возможность использовать сервисы и функции слоев Domain и Application. Во избежание сильной связности между хранилищем и границами приложения, сервисы должны быть использованы с помощью механизма Dependency Injection. Хранилища должны ссылаться только на интерфейсы.

И последнее — мы не хотим чтобы данные внутри хранилищ были публичными во избежание нежелательного изменения данных в процессе рендеринга. Хранилища должны быть ответственны за свою целостность. Компоненты же, в свою очередь, должны быть ничем большим чем строго-типизированные-и-оптимизированные-html-шаблоны. Для достижения подобного разделения хранилища должны инкапсулировать данные внутри себя и предоставлять методы для работы с этими данными. Другими словами, хранилища должны быть классическими экземплярами классов.

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

  • Лучшей читаемости кода, т.к. разработчик может предположить назначение компонента из набора данных принимаемых этим компонентом.

  • Лучшей производительности т.к. можно избежать перерисовки компонента в случае изменения неиспользуемых данных.

Таким образом, пятая цель [6.5] — позволить хранилищам данных быть определенными как классические TypeScript классы. Обозначить механику определения среза данных, используемого конкретным компонентом.

Держа эти цели в голове, давайте перечислим необходимые логические блоки кода:

  • Компоненты (Components) — строго типизированные HTML шаблоны + CSS стили

  • Модели вида (ViewModels) — классы, инкапсулирующие состояние данных, используемое компонентом (и всей иерархией компонентов под ним).

  • Фасады моделей вида (ViewModel facades) — ограничивают видимость свойств модели вида теми, которые используются в конкретном компоненте.

Рисунок 6: желаемая структура кода в слое представления

  • Не-пунктирные стрелки отражают рендеринг компонентов родительскими компонентами. Направление стрелки отражает направление передачи атрибутов.

  • Пунктирные линии отражают зависимости одних логических кусков кода от других (ссылки).

  • Блоки с зеленой рамкой — границы модуля. Каждый модуль/подмодуль отражен выделенной под него папкой. Общие модули лежат в папке «shared».

  • Голубые блоки — модели вида. Модели вида определены по штуке на модуль/подмодуль.

Что упущено? Заметьте как модели вида на рисунке 6 не имеют никаких параметров. Это всегда справедливо для модулей верхнего уровня (страниц) и глобальных моделей вида. Но подмодули зачастую зависят от параметров, определённых в процессе работы с приложением.

Обозначим шестую цель [6.6] — позволить атрибутам подмодуля быть использованными моделью вида этого подмодуля.

Рисунок 7: атрибуты передаются не только в корневой компонент модуля но и в его модель вида

7. Техническая реализация

Я буду использовать популярные библиотеки с тем чтобы сделать статью легче для восприятия. По этой причине тут не будет сравнения различных вариантов — для реализации конкретной ответственности всегда будет использована наиболее популярная библиотека.

7.1. Компоненты

Для отрисовки строго-типизированной разметки можно использовать синтаксис tsx (типизированный jsx). Рендеринг tsx поддерживается различными библиотеками, такими как ReactPreact and Inferno. Tsx НЕ является чистым HTML, тем не менее он может быть автоматически сконвертирован в/из HTML. Потому зависимость от tsx мне кажется допустимой т.к. в случае миграции на чистый HTML, значительная часть работы может быть выполнена автоматически.

Для уменьшения зависимости от конкретной библиотеки для рендера, мы ограничим компоненты так, чтобы они были чистыми функциями, принимающими атрибуты и возвращающими JSX узел. Этот подход был популярен и отлично себя зарекомендовал в ранний период развития React.

Хинт: в последние годы функциональные компоненты в виде чистых функций вышли из моды в сообществе React. Использование react hooks наделяет функциональные реакт компоненты сайд-еффектами и поощряет смешивание рендера с логикой управления состоянием. Хуки являются специфическим API для React и не должны использоваться при разработке в подходе, описанном в данной статье.

Другими словами, компоненты лишены состояния. Представим их через выражение UI=F(S) где

  • UI — видимая разметка

  • F — определение компонента

  • S — текущее значение данных внутри модели вида (здесь и далее — вьюмодели)

Пример компонента может выглядет так:

interface ITodoItemAttributes {
  name: string;
  status: TodoStatus;
  toggleStatus: () => void;
  removeTodo: () => void;
}

const TodoItemDisconnected = (props: ITodoItemAttributes) => {
  const className = props.status === TodoStatus.Completed ? 'completed' : '';
  return (
    <li className={className}>
      <div className="view">
        <input className="toggle" type="checkbox" onChange={props.toggleStatus} checked={props.status === TodoStatus.Completed} />
        <label>{props.name}</label>
        <button className="destroy" onClick={props.removeTodo} />
      </div>
    </li>
  )
}

Этот компонент отвечает за отрисовку одного todo элемента внутри TodoMVC приложения.

Единственная зависимость в этом коде — это зависимость от синтаксиса JSX. Соответственно этот компонент может быть отрисован различными библиотеками. С таким подходом замена библиотеки для отрисовки все еще не является бесплатной, но является «реалистичной».

Итого мы достигли целей [6.1] и [6.2].

Хинт: я использую react для TodoMVC приложения приведенного в качестве примера.

7.2. Модели Вида (вьюмодели)

Как было сказано ранее, мы хотим чтобы вьюмодели были написаны в виде TypeScript классов с тем что-бы:

  • Обеспечивать инкапсуляцию данных.

  • Предоставлять возможность взаимодействия со слоями domain/application посредством механизма dependency injection.

Однако, классы не предоставляют встроенных механик перерисовки компонентов использующих данные, инкапсулированные экземпляром класса.

Применим принципы реактивного интерфейса (reactive UI). Подробное описание этих принципов приведено в этом документе. Данный подход был впервые представлен в WPF (C#) и назван Model-View-ViewModel. В JavaScript сообществе, объекты предоставляющие доступ к обозреваемым (observable) данным чаще называются хранилищами (stores) следуя терминологии flux. Отмечу что хранилище это очень абстрактный термин, он может определять:

  • Глобальное хранилище данных для всего приложения.

  • Доменный объект, инкапсулирующий логику логику бизнеса и не привязанный к конкретному компоненту но и не являющийся глобальным.

  • Локальное хранилище данных для конкретного компонента или иерархии компонентов.

Таким образом любая вьюмодель является хранилищем, но не каждое хранилище является вьюмоделью.

Определим ограничения к реализации вьюмоделей:

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

  • Вьюмодель не должна ссылаться на компоненты и не должна знать о том что существуют конкретные компоненты, ссылающиеся на эту вьюмодель.

Я использую mobx декораторы для того, чтобы сделать поля класса обозреваемыми. Пример вьюмодели:

class TodosVM {
    @mobx.observable
    private todoList: ITodoItem[];

    // use "poor man DI", but in the real applications todoDao will be initialized by the call to IoC container 
    constructor(props: { status: TodoStatus }, private readonly todoDao: ITodoDAO = new TodoDAO()) {
        this.todoList = [];
    }
    public initialize() {
        this.todoList = this.todoDao.getList();
    }
    @mobx.action
    public removeTodo = (id: number) => {
        const targetItemIndex = this.todoList.findIndex(x => x.id === id);
        this.todoList.splice(targetItemIndex, 1);
        this.todoDao.delete(id);
    }
    public getTodoItems = (filter?: TodoStatus) => {
        return this.todoList.filter(x => !filter || x.status === filter) as ReadonlyArray<Readonly<ITodoItem>>;
    }
/// ... other methods such as creation and status toggling of todo items ...
}

Обратите внимание что мы ссылаемся на mobx напрямую, однако декораторы не присутствуют в теле методов.

Я продемонстрирую как абстрагировать реактивность и убрать зависимость от mobx в следующей статье. Пока что посчитаем достаточным обращаться к декораторам через пространство имен mobx. При таком ограничении заменить декораторы на декораторы из другой библиотеки можно будет с помощью автоматизированного скрипта.

Также обратите внимание что конструктор вьюмодели принимает первый аргумент типа {status: TodoStatus}. Это позволяет удовлетворить цели [6.6]. Тип должен совпадать с типом определяющим атрибуты корневого компонента модуля. Ниже обобщенный интерфейс вьюмодели:

interface IVMConstructor<TProps, TVM extends IViewModel<TProps>> {
    new (props: TProps, ...dependencies: any[]) : TVM;
}
interface IViewModel<IProps = Record<string, unknown>> {
    initialize?: () => Promise<void> | void;
    cleanup?: () => void;
    onPropsChanged?: (props: IProps) => void;
}

Все методы вьюмодели необязательны. Они могут быть определены для:

  • Выполнения кода при создании вьюмодели

  • Выполнения кода при удалении вьюмодели

  • Выполнения кода при изменении атрибутов (под-)модуля.

В отличии от компонентов, вьюмодель хранит свое состояние (является statefull). Она должна быть создана когда модуль появляется на странице и удалена как только модуль исчезает со страницы.

Как показано на рисунке 7, точкой входа для модуля является его корневой компонент. Таким образом вьюмодель должна быть создана когда корневой компонент модуля добавлен в структуру DOM(mounted) и удалена когда он удаляется со страницы(unmounted). Решить эту задачу можно с помощью техники компонентов высшего порядка (higher order components).

Определим тип функции:

 type TWithViewModel = <TAttributes, TViewModelProps, TViewModel>
  (
    moduleRootComponent: Component<TAttributes & TViewModelProps>,
    vmConstructor: IVMConstructor<TAttributes, TViewModel>,
  ) => Component<TAttributes>

Эта функция возвращает компонент высшего порядка над moduleRootComponent, который:

  • Должен обеспечить создание вьюмодели перед созданием и монтированием (mount) компонента.

  • Должен обеспечить зачистку(удаление) вьюмодели при демонтировании (unmount).

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

Пример использования данной функции:

const TodoMVCDisconnected = (props: { status: TodoStatus }) => {
    return <section className="todoapp">
        <Header />
        <TodoList status={props.status} />
        <Footer selectedStatus={props.status} />
    </section>
};
const TodoMVC = withVM(TodoMVCDisconnected, TodosVM);

В разметку корневой страницы приложения (либо роутера, зависит от того что как построено ваше приложение), результирующий компонент будет вставлен как <TodoMVC status={statusReceivedFromRouteParameters} />. После чего, экземпляр TodosVM становится доступным для всех под-компонентов внутри компонента TodoMVC.

Важно что реализация создания вьюмодели, а также доступности вьюмодели для дочерних компонентов, скрыта внутри withVM.

  • TodoMVCDisconnected компонент не зависит от библиотеки рендера

  • TodoMVC компонент может быть прорендерен в компоненте, не зависящем от библиотеки рендера

  • TodosVM ссылается только на декораторы. Потому, как описано выше, её отвязка от mobx реальна.

Хинт: в реализации из примера, функция withVM зависит от react context API. Вы можете попробовать реализовать аналогичное поведение в обход контекст апи. Важно, что реализация должна быть синхронизирована с реализацией доступа к вьюмодели из фасадов вьюмоделей — смотрите описание функции connectFn в следующем разделе.

7.3. Фасады вьюмоделей

Фасад определяет класс, выставляющий для публичного доступа ограниченное количество функций/данных модуля. Из рисунка 6 видно что мы хотим иметь по фасаду на каждый компонент. Однако создание дополнительного класса на каждый компонент будет излишне многословным.

Попробуем вместо классических «фасадов» использовать функции, принимающие вьюмодель (или несколько вьюмоделей) и возвращающие набор функций/данных, необходимых конкретному компоненту. Назовем их функциями среза (slicing function). Что если такая функция будет получать атрибуты компонента, который она обслуживает, в качестве последнего аргумента?

Рисунок 8: передача атрибутов компонента фасаду вьюмодели (функции среза/slicing function)

Посмотрим на синтаксис (в случае одной вьюмодели):

type TViewModelFacade = <TViewModel, TOwnProps, TVMProps>(vm: TViewModel, ownProps?: TOwnProps) => TVMProps

Выглядит очень похоже на функцию connect из библиотеки Redux. С той лишь разницей что вместо аргументов mapStateToPropsmapDispatchToActions и mergeProps мы имеем один аргумент — функцию среза, которая должна вернуть данные и методы одним объектом. Ниже пример функции среза для компонента TodoItemDisconnected и вьюмодели TodosVM.

const sliceTodosVMProps = (vm: TodosVM, ownProps: {id: string, name: string, status: TodoStatus; }) => {
    return {
        toggleStatus: () => vm.toggleStatus(ownProps.id),
        removeTodo: () => vm.removeTodo(ownProps.id),
    }
}

Заметка: Я назвал аргумент функции, содержащий атрибуты компонента ‘OwnProps’ что-бы приблизить его к терминологии применяемой в react/redux.

Такая схожесть диктует наиболее естественный способ использования функций среза — с помощью компонентов высшего порядка. Представим что вьюмодель уже хостится компонентом выше в дереве компонентов с помощью функции withVM. Давайте определим тип функции, которая принимает функцию среза и компонент, а возвращает — компонент высшего порядка, привязанный к вьюмодели:

type connectFn = <TViewModel, TVMProps, TOwnProps = {}>
(
    ComponentToConnect: Component<TVMProps & TOwnProps>,
    mapVMToProps: TViewModelFacade<TViewModel, TOwnProps, TVMProps>,
) => Component<TOwnProps>
const TodoItem = connectFn(TodoItemDisconnected, sliceTodosVMProps);

Отрисовка такового компонента в списке todo элементов: <TodoItem id={itemId} name={itemName} status={itemStatus} />

Заметьте что connectFn скрывает детали реализации реактивности:

  • Она берёт компонент TodoItemDisconnected и функцию среза sliceTodosVMProps — обе не знающие ничего о реактивности и о библиотеке для рендеринга JSX.

  • Она возвращает компонент, который будет перерисован реактивно как только данные, инкапсулированные вьюмоделью, изменяться.

Смотрите на реализацию функции connectFn для TodoMVC приложения, сделанного в качестве примера.

8. Заключение

Итого весь код, относящийся к конкретным бизнес задачам приложения, независим от фреймворков. TypeScript объекты, функции, TSX — это все к чему мы привязаны.

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

Все же, может ли слой представления быть полностью независим от фреймворков в реальном приложении?

Для того что-бы убрать ссылки на mobx, react и mobx-react из слоя представления, нужно сделать немного больше:

  • Абстрагироваться от mobx декораторов

  • Абстрагировать все фреймворко-зависимые библиотеки, используемые слоем представления. К примеру TodoMVC зависит от библиотек react-router и react-router-dom.

  • Абстрагироваться от синтетических событий, специфичных для конкретной библиотеки, отрисовывающей JSX.

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

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

P.S. Сравнение рассмотренной структуры и ее реализации с популярными фреймворками для разработки SPA:

  • В сравнении со связкой React/Redux: вьюмодели заменяют reducersaction creators и middlewares. Вьюмодели содержат состояние (являются stateful). Нет time-travel. Множество хранилищ. Отсутствие просадки производительности вызванной наличием большого числа использований функции connect с какой то логикой внутри. Redux-dirven приложения становятся все медленнее и медленнее с течением времени из за добавления новых connected компонентов в приложение. При этом не существует какого то конкретного ботлнека, устранением которого можно было бы исправить ситуацию.

  • В сравнении с vue: строго типизированные представления благодаря TSX. Вьюмодели являются обычными классами и не требуют использования функций сторонних библиотек, равно как не обязаны удовлетворять интерфейсу, определенному сторонними фреймворками. Vue.js заставляет определять состояние внутри определенной структуры имеющей свойства ‘data’,’methods’, и т.д. Отсутствие vue-специфических директив и синтаксиса привязки к модели.

  • В сравнении с angular: строго типизированные представления благодаря TSX. Отсутствие angular-специфических директив и синтаксиса привязки к модели. Инкапсуляция данных внутри вьюмоделей в отсутствие двусторонней привязки данных (two-way data binding). Хинт: для определенных сценариев, таких как формы, двусторонняя привязка данных удобна и полезна.

  • В сравнении с чистым react где управление состоянием выполняется с помощью хуков (hooks, такие как useState/useContext):Лучшее разделение ответственностей. Вьюмодели могут восприниматься в терминологии реакта как контейнер компоненты, которые лишены возможность рендерить что-либо и являются ответственными исключительно за работу с данными. Нет необходимости:

    • следить за последовательностью вызова хуков.

    • отслеживать зависимости хуков useEffect внутри ‘deps’ массива.

    • проверять смонтирован ли все еще компонент после каждого асинхронного действия.

    • следить что замыкания из предыдущих рендеров не используются внутри обработчика хука эффекта.

    Как любая технология, хуки (и в частности — useEffect) требует разработчика следовать некоторым рекомендациям. Эти рекомендации не являются частью интерфейсов, но приняты как «подход», «модель мышления (mental model)» или «стандартные практики (best practices)». Прекрасная статья про использование хуков от члена команды разработки react. Прочитайте ее и ответьте себе на два вопроса:

    • Что вы получаете используя хуки?

    • Как много правил, не контролируемых компилятором/линтером и сложно отслеживаемых на ручном код ревью, нужно соблюдать чтобы использование хуков оставалось предсказуемым?Если второй список получился больше первого — это хороший сигнал что относится к хукам стоит с большой осторожностью. Хинт: пример фрустрации от хуков

  • В сравнении с react-mobx интеграцией. Структура кода не определяется пакетом react-mobx и не предлагается документацией к нему. Разработчик должен придумать подход к структурированию кода сам. Рассмотренную в статье структуру можно считать таким подходом.

  • В сравнении с mobx-state-tree: Вьюмодели являются обычными классами и не требуют использования функций сторонних библиотек, равно как не обязаны удовлетворять интерфейсу, определенному сторонними фреймворками. Определение типа внутри mobx-state-tree опирается на специфические функции этого пакета. Использование mobx-state-tree в связке с TypeScript провоцирует дублирование информации — поля типа объявляются как отдельный TypeScript интерфейс но при этом обязаны быть перечислены в объекте, используемом для определения типа.

Оригинал статьи на английском языке в блоге автора (меня же)




Комментарии (34):

  1. KasperGreen
    /#22804542

    Это перевод?

    • dani_jug
      /#22806720 / +1

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

      • KasperGreen
        /#22806766

        Большое спасибо за статью, сам об этом задумывался по рекомендации Дядюшки Боба.

        • dani_jug
          /#22807514

          Так а все таки как на хабре принято в случае выкладывания здесь перевода своей же статьи? Маркировать её как перевод или не стоит?

          • KasperGreen
            /#22813734 / +1

            Стоит упомянуть об этом в тексте. Значок по желанию.

  2. justboris
    /#22804582 / +2

    После того, как мне два года довелось работать над подобным кросс-фреймворковым проектом, могу заметить, что все это гладко выглядит только на бумаге.


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

    • dani_jug
      /#22806814

      It depends (tm).

      Я полностью согласен с вами во втором утверждении и частично — в первом.

      По второму — дырявая абстракция хуже отсутствия абстракции. Особенно когда в проектной документации и в презентациях новичкам «дырявость» изящно скрывается. Если дырка есть — о ней нужно кричать красным H1 заголовком — чего конечно же не любит менеджмент от слова «совсем».

      По первому сложнее — если мы возьмем «идеальный фреймворк который полностью удовлетворяет нашим нуждам» — то да, абстрагирование от него безоговорочно замедлит разработку (сильно замедлит).
      Но я не вижу в данный момент фреймворка который выглядел бы «о**нно» и мне бы хотелось его просто взять и спроксировать. Прочитайте PS статьи — сравнение подхода с работой в контексте популярных фреймворков. Буду рад подискутировать на этот счет.

      Стоит ли возможность перенести приложение на новый фреймворк «потом» замедления разработки «сейчас»? It depends (tm)

      • justboris
        /#22807124

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

        • dani_jug
          /#22807222

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

          Не питаю иллюзий о том что смена фреймворка будет ванклик активностью. Но по крайней мере она будет реализуема в измеримое время (недели, месяцы). А не «нам нужно два года, полная спецификация продукта и отдельная команда разработки», что делает саму идею рерайта irrelevant. Ведь активный транк приложения за эти годы уйдёт далеко вперёд.

          • justboris
            /#22807254

            по крайней мере она будет реализуема в измеримое время (недели, месяцы). А не «нам нужно два года, полная спецификация продукта и отдельная команда разработки»

            Вы таки не поверите. У нас была ровно такая ситуация. Отдельные view и модели, и необходимость обновить наш UI-фреймворк с Mithril на React. Несмотря на наличие вроде-как фреймворк-независимых моделей, миграция заняла год, при этом продукт переписался полностью, включая часть с моделями, потому что lifecycle API Mithril совсем никак не ложился на React, и никакой адаптер не помог, пришлось все переписывать на идиоматичный React-код, за время сравнимое с переписыванием с нуля.

            • dani_jug
              /#22807368

              Поверю конечно. Например подход из этой статьи слабо поможет переписать приложение на вьюжс. И в случае возникновения такой задачи… ваш кейс.

              Однако для удовлетворения ответственностей определённых в статье (рендеринг JSX, обеспечение реактивности) — разработчик волен выбирать лучший инструмент из представленных на рынке.

              Ну и вы же таки смогли переписать приложение. Это уже победа! Я встрявал в более глубокие жопы. Из недавнего — приложение на бекбон/марионет, где никто из команды не знает почему оно именно такое как есть и что должно делаться. И невозможностью нанять новых разработчиков — люди реально увольнялись когда видели стек. Рекрутёры врали про мифический ангулар в миграция в который продлилась два месяца но не ушла дальше первой странички. (спойлер, лично я фанат бекбон/марионет, на мой взгляд эта связка, вместе с нокаутом, была для ФЕ комьюнити толчком к рич приложениям).

              Решением была инкрементальная миграция, когда конкретные регионы приложения избирательно переписывались со всей болью связанной с паралельным наличием билда в двух фреймворках и передачей контроля marionette -> react -> marionette -> react.
              Больно, долго, не факт что когда нибудь будет завершено, но по крайней мере новые фичи пиляться и люди нанимаються. Но опять же, я не верю что миграция «когда нибудь» будет завершена. А что если реакт и мобкс выйдут из тренда? :)

              upd. Очень больно, очень дорого и очень сложно. И далеко не факт что после ухода меня из компании такая инкрементальная миграция прогрессирует.

              • justboris
                /#22809220

                А как проблемы предыдущего инкрементального подхода решаются в вашем новом решении?

                • dani_jug
                  /#22809272

                  Одно с другим не связано. Я имел в виду что ваш перенос приложения целиком на другой фреймворк (пусть и за год) можно назвать успехом.
                  Инкрементальный перенос который я упомянул (франкенштейн) можно назвать компромисом который позволил приложению «жить», но что это была за жизнь!

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

                  • justboris
                    /#22809400

                    Попробую перефразировать вопрос – что именно было больно и дорого "marionette -> react -> marionette -> react" и как это решается в вашем новом решении которое в первую очередь рекламируется вами как "возможность сменить мобкс на условный метеор"?

                    • dani_jug
                      /#22809612

                      Было две болевых точки:

                      1 — очень плотная связность между существующими обьектами: (марионеттовские view, которые логически — контроллеры, и модули). Были уроборосы наподобие такого: view1:riseEvent[E1] -> view2:proxyEvent[E1] -> view3:handleEvent[E1]:riseRequest[R1] -> module1:handleRequest[R1]:fireGlobalEvent[GE1] -> view1:handleEvent[GE1] and do some work. Это очень упрощенная цепочка, в реальности цепочки исчислялись десятками вызовов между разными вью/модулями/бекбоновскими моделями с ветвлениями. В ходе выполнения этой цепочки, различные вью случайно брали данные из подчинённой себе разметки И/ИЛИ модели.

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

                      В описанном в статье подходе во первых нет разделения на модели и вьюхи(marionette.views которые, напомню, на самом деле контроллеры) с недоопределёнными ответственностями для обоих. Есть только вьюмодели, данные из которых реактивно потребляются представлениями. Каждое потребление вьюмодели отслеживается через применение соответствующей функции среза, соответственно отследить все использования вьюмодели легко по использованиям функций среза и тайпскриптовским ссылкам. Нет ситуации когда непонятно где, для чего и как может использоваться конкретная функция, определённая на вью или на модели (скажем через глобальное событие трижджы спроксированное и приправленное reqres обработчиком). Вьюмодели инкапсулируют состояние, в отличии от бекбоновских моделей в которые можно установить любое свойство и на изменение которых можно подписаться из любого места приложения.

                      2 — непосредственно механики интеграции марионетта и реакта. У нас были адаптеры — марионет вьюшки, которые хостили реакт компоненты. И обратные адаптеры — реакт компоненты, которые могли хостить марионет вьюшки.
                      Процесс миграции был определён так:
                      — выбирается вью либо страница.
                      — все данные, содержащиеся на вью (контроллере) и на модели (если такая наличиствует, зачастую на иерерхии моделей) перетекают в мобкс стор (иерархию сторов). Все подписки вью и модели добиваются мобикс реакциями. Вью перерендеривается при именении данных на сторе. Регрессионное тестирование.
                      — перевод разметки из марионет шаблона в реакт компонент, реагирующий на уже подготовленный мобкс стор. Монтирование этого компонента в марионет приложение с помощью соответствующего адаптера. Если вью использует внутри себя какие то общие вью — монтирование их в реакт компонент с помощью обратного адаптера.

                      По пункту 2 нет «специфических проблем» решаемых предложенным в статье подходом, это просто требовало громадного headroom от разработчика. Условный джун или условный уставший сеньор просто не в состоянии были взяться за миграцию сколько нибудь большого фрагмента.

                      Предложенное решение говорит что смена ТСХ рендерера или смена движка реактивности — это изменение инфраструктурного кода, которое не затрагивает код вьюмоделей или вьюшек, т.е. конкретных фич.

                      • justboris
                        /#22810598

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

                        • dani_jug
                          /#22810822

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

                          Т.е. Straightforward будет смена TSX рендерера либо либы отвечающей за реактивность. Э замена на свелте не будет аккуратной, т.к. свелте нельзя назвать библиотекой для рендера JSX, соответсвенно изменится логическая нагрузка на один из елементов структуры кода.

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

                        • dani_jug
                          /#22822996

                          На досуге переехал на свелте в этом бранче

                          По итогу:
                          — good: вьюмодель не затронута
                          — good2: вью изменились исключительно с точки зрения движка-для-отрисовки, т.е. ситнтаксис слегка другой но конструкции идентичны.
                          — bad: функции среза перекочевали внутрь тега script конкретного компонента. С одной стороны заняло ноль усилий их перенести. С другой — в svelte варианте они не изолированы от шаблона. Для меня это неприемлимое нарушение принципа единственной ответственности. Но из за того что свелте не даёт определить в одном файле более одного компонента — нет и возможность «определить компонент и в том же файле завернуть его в HOC не смешивая со скриптом компонента». Решить можно или так как сейчас или создавая на каждую обёртку по файлу, что есть ещё большая дичъ т.к. связанный между собой код разбрасывается по разным файлам.
                          — bad2: тайпскрипт со свелте и связанные плагины для сборки это то ещё уныние. На сам перенос потратил часа полтора, а сборку поднимал два вечера.

          • justboris
            /#22807302

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


            Хорошее описание этого подхода есть в этой статье (не знаю, есть ли перевод)

            • dani_jug
              /#22807378

              Собственно в комменте выше я описал именно это, давайте в тот тред:)

  3. sentyaev
    /#22804784 / +6

    В момент когда фреймворк на базе которого построено приложение выходит из моды — вы вынуждены либо поддерживать устаревшую (legacy) кодовую базу либо стартовать процесс перевода приложения на новый фреймворк.

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

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

    Поддержка устаревшей кодовой базы означает проблемы с наймом новых и мотивацией текущих разработчиков.

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

    Перевод приложения на новый фреймворк стоит времени (следственно — денег) но не несет никакой пользы для бизнеса.

    Действительно, обновление ради обновления не несет никакой пользы для бизнеса.
    Обноляют фрейворк для решения трех базовых задачь:
    1. Устранение критических уязвимостей. Это прямая выгода для бизнеса, т.к. кто-то уже потратил время на их поиск и устранил их. Осталось только обновится. Да, это может занять и неделю, и даже месяц, но альтернатива — от потери репутации до потери бизнеса.
    2. Улучшение developer expirience, что влечет за собой увеличение мотивации разработчиков, ускорение внедрения новой бизнес функциональности.
    3. Улучшение performance. Потратили неделю на обновление и у всех клиентов приложение работает на 30% быстрее, т.е. улучшили важний бизнес показатель — удовлетворенность клиентов.

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

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

    • dani_jug
      /#22807068

      Я абсолютно согласен с вами что фреймворки/либы — это мощное комьюнити и 100500 часов разработки, багофиксов и всего что скрывается за словом «мощное компьюнити».

      Именно потому я не пытаюсь педалить свой велосипед с блекджеком и женщинами в части «реализации ответственностей» — все делается проверенными и поддерживаемыми инструментами.

      Однако «определение» этих ответственностей, на мой взгляд, должно исходить из специфики приложения. Тесть из связки [Бизнес домен]+[Раздел 2, подраздер «ограничения» данной статьи]+[проверенные архитектурные принципы].
      Соответственно, начиная работу над потенциально long-living приложением, разработчик обязан грамотно спроектировать структуру этого приложения и подобрать инструменты. Желательно — с возможностью замены этих инструментов.

      Обратная ситуация: фейсбук переизобретает eventsourcing для фронта потому что NIH. Ден абрамов переизобретает elm на джаваскрипте, потому что талантливый гик который пробует клёвые идеи. Совпало, завертелось. 3-4 года значительная часть клиентов заказывают приложения на реакт-редакс потому что «фейсбук знает, так надо». Без анализа «а подойдёт ли это нам». 2020 год — интернет пестрит статьями «вай редакс зло» — хотя редакс совсем не зло а просто очень нишевая вещь, которая не была спозиционирована должным образом.
      И вот написанный в реакт-редаксе код невероятно плотно забит приложение-специфическими вещами (миддлвары, разные типы екшнов, саги со своим синтакисом).
      Бизнес логику просто невозможно «извлечь», а редакс уже «зло» и через несеколько лет из малого зла станет синдромом «легаси проекта».

      Касательно онбординга в предложенном подходе:

      Начинающий(или мидл) разработчик, прийдя на проект, услышит что у нас реакт+мобикс. И это будет абсолютной правдой. Относительно связки реакт+мобикс предложенная структура предлагает только лиш руководство к «месту где разработчик должен определять сторы», т.е. где в структуре компонентов (или вне структуры компонентов) эти сторы живут. И ирония в том что так или иначе работая с реакт-мобкс команде придётся такое решение делать.
      Плюс к этому, подход запрещает использовать хуки (обьяснение в статье). Имея доступ к сторам, возможность использования хуков — это всего лиш способ делать одни и те же вещи двумя разными способами. Т.е. в контексте одного конкретного проекта — зло.

      Однако я оставляю за собой возможность сменить реакт на преакт/инферно. Равно как возможность сменить мобкс на условный метеор. Why not?

  4. noodles
    /#22805558

    Фреймворк-независимое браузерное SPA

    Все же, может ли слой представления быть полностью независим от фреймворков в реальном приложении?

    Если относится к фронтенд-приложению чуть проще, т.е. как это было раньше — это просто View слой всего приложения, то тогда вроде и жить легче всем.
    А вы предлагаете усложнить этот view слой ещё больше и нагородить ещё и в нём абстракций:

    Абстрагироваться от mobx декораторов…

    Абстрагировать все фреймворко-зависимые библиотеки…

    Абстрагироваться от синтетических событий…

    таким образом усложнив жизнь новым сотрудникам и увеличив риски для бизнеса в этом контексте.

    • dani_jug
      /#22806838

      Зависит от сложности фронтенда. Количество механик на современном фронте зачастую не даёт относится к нему исключительно как к вью слою. По последней цитате, все кроме абстрагирования от синтетических событий очень легко. А абстрагироваться от синтетических событий это и в самом деле написание нового фреймворка и делать этого, конечно же, не стоит.

      • noodles
        /#22810548

        Количество механик на современном фронте зачастую не даёт относится к нему исключительно как к вью слою.

        Эти механики — это «бизнес логика» интерфейса / UI только и всего. Настоящая бизнес логика так или иначе реализуется на бекенде, с сопутствующей валидацией бизнес процессов, транзакциями и т.д.
        Именно поэтому, мне кажется, что к фронту нужно относится проще — как к view-слою всего приложения вцелом. Фронтов-то можеть быть несколько, включая мобильные приложения.
        И если «разрешить» себе и другим жить с такой установкой (фронт — это всего-лишь вью слой, какой-бы крутой ui там не был) — то таким образом можно здорово облегчить жизнь и использовать выбранный фреймворк на максималках со всеми его правилами и ограничениями.

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

        Опять же, вот я сейчас переписываю с бекбон на реакт+мобх. Вы предлагаете мне заранее сейчас подумать и спроектировать приложение таким образом, чтоб если вдруг кому-то в голову в будущем стукнет моча переписать это приложение на ангуляр — мне бы нужно было «легко и просто» обновить только компоненты. Пожалуй нет, я лучше сделаю сейчас быстро и понятно, максимально взяв то что дают мне реакт+мобкс.
        При необходимости переписать всё выкидываются и делается с нуля на ангуляре, да опять (но это ответственность и цена того, кто будет принимать такое решение).

        • dani_jug
          /#22810740

          Кстати, как бы это странно не звучало но именно вам имеет смысл вчитаться в статью переписывая в «реакт+мобкс». Потому что эта связка не определяет «где должны определяться сторы». Т.е. где физически должны лежать файлики и как сторы «деляться» между компонентами. Описанный в статье подход как раз доопределяет это. Но хозяин, конечно, барин.

          Про переписывание фронта под существующее апи — зависит от розмера. Если фронт маленький — проблемы нет. Если фронт большой-и-старый то это фактически нереализуемо, из моего опыта.

          • noodles
            /#22815048

            Потому что эта связка не определяет «где должны определяться сторы». Т.е. где физически должны лежать файлики и как сторы «деляться» между компонентами.

            А что там определять-то… Всё делать по доке. После того как они выпилили inject и MobxProvider (или как он там назывался) — официально рекомендуется использовать реактовский контекст (для всего корня приложения) для хранения сторов. Сами сторы создавать уже по смысловым бизнес-доменам (т.е. те сущности, про которые должен знать любой компонент в любой момент времени на протяжении всей жизни приложения).
            Также иногда уместно использовать useLocalObservable для чего-то не глобального, но когда useState не очень, т.к. затрагивает достаточно большое дочернее поддерево.

            • dani_jug
              /#22816542

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

              Когда (если) появится проблема перемешивания «общей» логики и «вью-специфик» логики, либо локальные обсерваблы сделают вью не-читаемо-громоздкими — тут вам поможет данная статья.
              А не возникнет проблемы — gl&hf, happy coding!

  5. RC_Cat
    /#22807084

    Картинка про 14 конкурирующих технологий

    • dani_jug
      /#22807136

      Я снимаю свой плащ и волшебную шляпу перед опенсорс комьюнити и не пытаюсь изобретать велосипед, но скорее «определить че делать» и «подобрать лучшие[из существующих] инструменты для решения конкретных задач».

      Наличие в мейнстрими фреймворков, делающих из себя «one ring to rule them all» и совершенно не пытающихся ограничить область своего применения — причина появления этой статьи. Есть классные инструменты решающие конкретные задачи. (тот же мобикс, например, или rxjs). Давайте дружить с ними:)

  6. kit_oz
    /#22807086

    Для компонентов уже есть веб-компоненты, которые точно так же можно писать хоть на чём от голого js, до реакта и компании.
    Для хранилища данных — local storage. Да, есть ограничения, но в остальном прекрасно справится с задачей, как мне кажется.
    Какие плюсы несёт данная схема?

    • dani_jug
      /#22807108

      Вопрос не «докопаться ради». Вы прочитали статью?
      Мне интересно, поскольку если «да» и у вас возник этот вопрос — значит я плохо выражаю свои мысли и нужно исправляться.
      Если «нет» — прочитайте если будет время после чего я открыт к обсуждению

      • kit_oz
        /#22808440 / +1

        Я прочитал. Возможно, проблема в том, что я не работал с очень большими проектами, с кучей логики, легаси кода и прочего.
        Суть моего комментария.
        Вы для отказа от одного фреймворка предлагаете свой фреймворк, который точно так же может устареть, плюс требует погружения в свою логику работы.
        Я же предлагаю использовать стандарт (строго говоря, набор стандартов, но главное — более низкоуровневое решение), который как раз и был введен для решения проблем с кучей фреймворков.

        • dani_jug
          /#22808754

          Ок, спасибо за комментарий,

          Вы не единственный в комментах кто воспринимает предложенный подход как «свой фреймворк» но я бы его так не определял.

          Скорее это подход к структурированию кода. Для упрощения восприятия можно обзывать подходом к структурированию кода поверх реакт-мобкс связки.

          Отвечая на вопрос про использование веб-компонентов и локалстореджа — в пункте 6 статьи определены цели, которых мы хотим достигнуть, в частности:
          — возможнсть использования строго типизированных представлений (для чего мы используем TSX).
          — возможность ограничить представления разметкой, т.е. сделать максимально тупым.

          На мой взгляд функциональные TSX компоненты решают эти задачи лучше всего. По крайней мере я не знаю как определять строго типизированные веб-компоненты, равно как не знаю как определять веб компоненты без дополнительного жс кода (пусть и примитивного).

          Что касается хранения данных. Моей целью было:
          — определить обьекты, отвечающие не только за хранение данных, но и за инкапсуляцию действий по модификации этих данных
          — обеспечить реактивность (перерисовку представлений при перерисовке данных)
          — обеспечить возможность привязки конкретных представлений к «глобальным (вьюмодель на много компонентов)» либо к «локальным»(вьюмодель на екземпляр компонента) хранилищам данных.
          — обеспечить возможность внедрения сервисов с логикой бизнеса в такие обьекты через DI.

          Локалсторедж позволяет хранить данные, но все остальные требования никак не удовлетворить локалстореджем, т.е. нужно что-то ещё.
          Описанный подход предлогает использовать ванильные TS классы (из примера), а уже методы withVM и connect скрывают детали реализации реактивности, привязки к представлениям, материализации.

          Обратите внимание на папку с приложением — тут у нас реально живет приложение.
          Тогда как реализация вышеупомянутых функций withVM и connect лежит тут. И именно тут лежит код, отвечающий за реализацию реактивность через мобикс или за реализацию внедрения вьюмоделей в дерево компонентов с помощью контекста реакт.

          Называть это фреймворком… перебор, же. Возможно количество текста в статье подспудно наделяет структуру кода дополнительной сложностью:)
          А может дело в моей личной привычке и восприятии, dunno.