Русский
Русский
English
Статистика
Реклама

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

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

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

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

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

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

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

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

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

Цели:

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

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

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

    • Интеграции свнешними сервисами (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.js,blazorиelmесли вызаинтересованы вдругих опциях

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

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

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

Существенный недостаток чистого HTML состоит втом что оннетипизирован. Существует достаточное количество движков шаблонизации, таких какunderscore.js,handlebars.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 поддерживается различными библиотеками, такими какReact,PreactandInferno. TsxНЕявляется чистым HTML, тем неменее онможет быть автоматически сконвертирован в/из HTML. Потому зависимость отtsx мне кажется допустимой т.к.вслучае миграции начистый HTML, значительная часть работы может быть выполнена автоматически.

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

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

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

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

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

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

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

interfaceITodoItemAttributes{name:string;status:TodoStatus;toggleStatus:()=>void;removeTodo:()=>void;}constTodoItemDisconnected=(props:ITodoItemAttributes)=>{constclassName=props.status===TodoStatus.Completed?'completed':'';return(<liclassName={className}><divclassName="view"><inputclassName="toggle"type="checkbox"onChange={props.toggleStatus}checked={props.status===TodoStatus.Completed}/><label>{props.name}</label><buttonclassName="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декораторы для того, чтобы сделать поля класса обозреваемыми. Пример вьюмодели:

classTodosVM{@mobx.observableprivatetodoList:ITodoItem[];//use"poormanDI",butintherealapplicationstodoDaowillbeinitializedbythecalltoIoCcontainerconstructor(props:{status:TodoStatus},privatereadonlytodoDao:ITodoDAO=newTodoDAO()){this.todoList=[];}publicinitialize(){this.todoList=this.todoDao.getList();}@mobx.actionpublicremoveTodo=(id:number)=>{consttargetItemIndex=this.todoList.findIndex(x=>x.id===id);this.todoList.splice(targetItemIndex,1);this.todoDao.delete(id);}publicgetTodoItems=(filter?:TodoStatus)=>{returnthis.todoList.filter(x=>!filter||x.status===filter)asReadonlyArray<Readonly<ITodoItem>>;}///...othermethodssuchascreationandstatustogglingoftodoitems...}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

constTodoMVCDisconnected=(props:{status:TodoStatus})=>{return<sectionclassName="todoapp"><Header/><TodoListstatus={props.status}/><FooterselectedStatus={props.status}/></section>};constTodoMVC=withVM(TodoMVCDisconnected,TodosVM);

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Отрисовка такового компонента всписке todo элементов:<TodoItemid={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: вьюмодели заменяютreducers,action 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 интерфейс нопри этом обязаны быть перечислены вобъекте, используемом для определения типа.

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

Источник: habr.com
К списку статей
Опубликовано: 14.03.2021 16:17:43
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Разработка веб-сайтов

Javascript

Программирование

Проектирование и рефакторинг

Reactjs

Type

React

Mobx

Архитектура

Mvvm

Flux

React hooks

Redux

Vue

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru