Стартовая позиция
Всё это началось в Мае 2017 года. Мы долго обсуждали это, были внутренние дебаты с коллегами, но в конце концов мы решили, что надо модернизироваться. Надо обновлять наш продукт и по сути - переписывать огромную его часть заново.
В нашем основном продукте много составляющих и одна из них - это классическое настольное приложение “Дизайнер”. Оно было разработано с использованием .Net WinForms, контейнера Autofac и шаблона композиции приложения Composite Application Block (CAB далее). Конечно, в дополнение к этому мы использовали библиотеку визуальных компонентов DotNetBar и у нас накопилось более трёх сотен собственных компонентов (без учёта форм бизнес логики).
Само приложение (его основа и базовая часть) было написано в далёком 2011 году и с тех пор постоянно улучшалось и добавлялись всё новые и новые возможности. Базовая архитектура приложения не сильно менялась, оно и понятно, ведь мы работаем с корпоративными клиентами и для нас всегда на первом месте была и будет обратная совместимость и простая процедура обновления. В разработке приложения были использованы те технологии и принципы, которые наша команда понимала в 2011 году и которые мы могли эффективно использовать.
Если очень просто описать архитектуру, то это приложение было клиентом для сервера поверх технологии .Net Remoting. Мы максимально переиспользовали код между сервером и клиентом, при этом избегали тяжёлого маршаллинга за счёт разделения логики по нескольким классам-сервисам и при этом использовали простейшие DTO модели для передачи данных. Через пару лет мы осознали, что всё это было бы проще реализовать поверх WCF, но к тому времени нас всё устраивало и не было смысла переписывать то, что и так работало.
Именно с этим мы подошли к концу 2016 года, когда впервые поднялся вопрос о том, что клиенты всё больше и больше жалуются именно на это приложение. Для его запуска необходимо скачать с сервера небольшой пакет установки, который далее скачивает с сервера нужные nuget пакеты и устанавливает приложение в систему. Это занимало всё больше и больше времени с развитием продукта. И самое важное - нашим “Дизайнером” было невозможно воспользоваться вне ОС Windows. Многие клиенты поднимали выделенную виртуальную машину только для работы в “Дизайнере“ (но не все могли себе позволить такую трату ресурсов).
Именно эти проблемы и стали решающими для принятия такого важного бизнес решения - необходимо перенести приложение “Дизайнер” в веб.
Долгая история и очень много работы
Обычно такие истории не рассказывают. Или их рассказывают массовые облачные сервисы (вроде AirBnB, Asana) для увеличения лояльности и привлечения внимания аудитории. Или ещё чаще это становится очередным поводом для маркетинговой кампании. Я же хочу рассказать эту историю со своей стороны, без украшения и подгонки результата. Возможно, для самого себя в первую очередь. Поэтому это будет длинная история. В несколько частей. Порой скучная и повторяющаяся. Но обязательно с бонусом в финале.
С конца 2016 и до Мая 2017 мы с коллегами обсуждали это решение. Его было не просто принять, ведь в самом приложении уже заложено огромное количество логики. Использованы сторонние визуальные компоненты не только для форм, но и для рисования диаграмм (Процессы, Организационная структура), формирования отчётов (FastReport.Net), редактирования кода C# (пользовательские скрипты). Не трудно понять, что это огромная работа в сумме с 6-ю годами накопленного кода бизнес-логики. Но мы пошли на это. Решение было принято и нужно было искать пути его реализации.
- Тут будут ссылки на другие части истории #add #link
Выбор стека для веб разработки
Где-то глубоко внутри я уверен, что большинство из вас пропустило предыдущий абзац. Возможно оно и к лучшему, но без понимания предыстории будет сложно понять дальнейшие решения.
Итак, Май 2017, задача по миграции сформирована, уже во всю за рисование интерфейсов взялся дизайнер под началом владельца продукта. У нас же (как и всегда) не было никаких свободных ресурсов на реализацию проекта. Собрать новую команду в короткий срок - нереальная задача, да и город у нас не то чтобы богат на свободных кандидатов. Добавляло проблем то, что для переписывания требовалось понимание логики самого приложения и архитектуры. Не только на стороне “Дизайнера”, но и сервера. В дополнение ко всему у нас в компании ни у кого не было достаточного опыта в разработке больших веб-приложений (надо сказать, что в текущем решении Дизайнера у нас около 120 проектов на C#, в каждом из которых больше 100 файлов). Более того, абсолютно никто не имел опыта создания расширяемых и модульных решений для браузера.
Тут стоит отступить в сторону и сказать, что основной пользовательский интерфейс у нас написан с использованием ASP.Net MVC, Razor, jQuery и компонентов Telerik. Мы несколько раз обновляли внутренние библиотеки, но большинство из них остались на версиях 3-4 летней давности. И сам принцип работы с JS логикой у нас был в большинстве своём на уровне 2010 года (по сути - куча лапши и глобальных функций). Все эти современные вещи в веб разработке были для нас очень далеки. Нам предстояла жестокая гонка за современным миром.
Однако, у нас было немного опыта, определённые потребности и пара популярных кандидатов на выбор. Конечно, мы выбирали между Angular и ReactJS (а чего ещё можно было ожидать в 2017 году набрав в поисковике “разработка веб приложения”). Да, на тот момент я уже давно следил за обоими продуктами и за другими игроками на рынке. Знал про VueJS, кучу клонов React (основанных на виртуальном DOM дереве), смотрел на старичков Backbone, Ember и даже ExtJS. Присматривался к более зрелым интегрированным решениям от DevExpress и Telerik поверх ASP.NET MVC. Конечно, к тому времени я уже знал про NodeJS, Webpack, Babel, TypeScript и вот это вот всё от мира современной веб разработки. Но знать и уметь - очень разные и далёкие друг от друга понятия. Нам было просто необходимо сформировать мнение на основе собственного опыта и реальной реализации на разных стеках.
Почему Angular?
Тут всё предельно просто. Он использует TypeScript из коробки, который легко изучить C# разработчику и он просто необходим при разработке в огромной кодовой базе. Архитектура и модульность позволяют очень удобно вести разработку в команде. Есть понятный и стандартный DI, который крайне важен для уменьшения связности модулей и дальнейшей поддерживаемости решения. За спиной его стоит Google (хотя это уже спорно после истории с AngularJS первой версии).
Структура кода на Angular очень органично складывается в структуру CAB. Можно легко сопоставить WorkItem как Компонент, а Controller как Сервис. При этом визуальная часть точно так же остаётся отдельно и взаимодействие идёт за счёт событий по цепочке UI - WorkItem (Component) - Controller (Service).
Конечно, так можно сравнить любые подходы, но мне было важно понять, как это объяснить другим разработчикам. А использовать уже понятные аналогии гораздо эффективнее.
Почему ReactJS?
Большое сообщество, много хайпа, огромная экосистема готовых решений. Простота в понимании. Быстродействие из коробки. Возможность работать используя TypeScript.
Первое знакомство с ReactJS у меня было ещё в 2013 году, через пару месяцев после открытия его публике. Тогда я просто прочитал пару статей и так и не понял “зачем этот html внутри js файлов”. После этого вернулся к нему только в 2015 году, когда уже было понятно, что современная веб разработка должна делаться иначе. Да и кучи статей про него на хабре не давали покоя. Тогда я присмотрелся к нему, разобрался с тем, что такое Компоненты и как работает этот самый Virtual Dom. И сразу стало понятно, что React - это никак не полноценный фреймворк для построения больших приложений. Он чётко занял позицию View для приложения. При этом относительно CAB было понятно, что Компонент - суть WorkItem, а вот остальное надо было додумывать.
Вообще, на этом этапе мне казалось, что Angular - это идеальный выбор. Мне совсем не хотелось копаться в тысячах библиотек и сравнений в поисках “лучшей”. Нам было достаточно “хорошей”, которая бы решила наши задачи при этом не потребовала бы от нас сумасшедших вложений. В дополнение ко всему ребята из соседней команды выбрали Angular как основу для своего нового продукта (до этого они уже попробовали ReactJS и он их не устроил). Казалось, что решение уже принято… Но мне удалось остановить самого себя от поспешных выводов. Мы собрали стартовую команду для внутренней оценки этих решений со всех сторон.
Критерии анализа кандидатов
Наконец-то мы добрались до того момента, когда настала пора сформировать чёткий список критериев для оценки и анализа возможного варианта решения нашей задачи. Привожу его так, как он был записан в Мае 2017:
- Работа с локализацией и перевод
- Даты-время (поддержка TZ)
- Возможность тестирования UI / BL
- Скорость разработки (одним человеком)
- Масштабируемость для совместной работы
- Расширяемость решения кодом
- Возможности построения расширяемости настройками
- Быстродействие при “наивной” реализации
- Сложность “оптимизированной” реализации
- Размер загружаемого кода на клиенте
- Возможность легко разбивать на части
Далее расшифрую некоторые пункты. Но надо сказать, что этот список требований не был случайным - для каждого пункта тут свои причины, связанные с текущей реализацией нашего приложения и планами на будущее развитие. Да и к этому моменту я уже прочитал и посмотрел, наверное, больше 100 статей и презентаций про React и Angular (как по отдельности, так и в сравнении), и у меня было представление об основных моментах, на которых стоит сосредоточить внимание именно в нашем случае.
Масштабируемость для совместной работы
Возможно, это был один из самых важных пунктов оценки. В нашей компании очень большой процент именно младших разработчиков. И к тому же есть сеть партнёров, да и сами клиенты могут писать код для расширения нашего приложения. И мы всегда очень строго подходили к “простоте и интуитивности” любых действий в продукте. И к разработке в том числе. Учитывая то, что у нас работает уже больше 50 разработчиков - понимание того, как будет выстроен процесс разработки и как с этим согласуется структура библиотеки (конвенции, возможные конфликты, изоляция работы) является для нас критичным.
Работа с локализацией и перевод
Кажется, что это довольно очевидно в современном веб приложении. Но для нас было крайне важно решить наши задачи максимально эффективно и не уходить далеко от уже накопленного опыта:
- Поддержка .po файлов для локализации
- Прозрачная локализация без необходимости писать кучу кода
- Автоматическая сборка ресурсов перевода по кодовой базе (в текущем решении у нас уже было для этого самописное приложение, формирующее .po файлы из исходного кода)
- Возможность “перевода прямо в интерфейсе” в будущем (это важно для партнёров, занимающихся переводом и для клиентов, которые сами хотят внедрить перевод)
Быстродействие при “наивной” реализации
Этот пункт я честно забрал из одной статьи про очередное сравнение фреймворков. Мне он очень понравился, опять же, по причине наличия большого количества неопытных разработчиков не только у нас, но и у клиентов. Нам совсем не хотелось отвечать в техподдержке постоянно на вопросы о медленном интерфейсе системы из-за решения, предоставленного партнёром или компонента, написанного самим клиентом. Для нас было важно минимизировать этот риск на самом раннем этапе. Суть оценки в том, что на первом этапе делается реализация решения “в лоб”. Так как человек понял, прочитав документацию, без использования каких-то особых техник оптимизации. Ну и далее на 2-м этапе (а оптимизация оказывается нужна, ведь данных выводится много) оценивается сложность и скорость того самого “оптимизированного” решения. Оценивается и то, каким путём разработчик вообще дошёл до этого решения. Было ли оно очевидным или путь поиска был тернист.
Расширяемость решения кодом
Возможно, мы в нашем продукте слишком много внимания отдаём расширяемости, но в конце концов - у нас кровавый энтерпрайз и мы должны это учитывать. Вообще весь наш продукт - это набор модулей, многие из которых ничего не знают друг про друга и склеиваются общим ядром и механизмом расширений (интерфейс - реализация). И, конечно, веб интерфейс никак не мог стать исключением. Скорее даже наоборот - в нём расширяемость должна быть самой гибкой. Суть нашей расширяемости в том, что клиент / партнёр / разработчик может собрать модуль (C# серверная библиотека + ASP.NET веб проект), упакованный в один архив и установить этот пакет на любой системе. Соответственно система сама должна подхватить нужные расширения из нового модуля. Сама по себе эта задача может быть решена сотней способов. Нам было важно понять как выбранный UI стэк позволяет склеивать эти расширения и использовать их на лету.
Возможности построения расширяемости настройками
Этот пункт очень специфичный для нас и очень важный одновременно. Тут идёт речь про то, что мы можем на стороне данных в БД задать форму отображения записи справочника (как конструктор в WPF) при помощи перетаскивания полей этого справочника и некоторых системных элементов (панель, вкладки). И далее уже на странице это отображать. Эта динамика должна реализовываться быстро и просто. Более того, эта динамика (как и всё в нашем приложении) должна расширяться сторонними модулями, про которые мы ничего не знаем заранее.
Остальные критерии должны быть вполне понятны и логичны для большинства разработчиков приложений. Добавлю только то, что на этом этапе не стоял вопрос выбора красивой UI библиотеки готовых компонентов. Мы смотрели на саму технологию, на фреймворк и его возможности.
Ещё мне бы очень хотелось написать подробно про “выбор” библиотеки для хранения состояния в веб приложении, но его просто не было. Мы довольно быстро решили, что будем использовать MobX. Некоторые причины я опишу ниже. Но в целом - здесь никаких мук выбора не было.
Почему MobX?
О хранении состояния в современном веб интерфейсе
В современной разработке веб-приложений до сих пор идут жаркие споры о том, как же хранить состояние на клиенте. Очевидно, что сторонников разных подходов всегда было и будет великое множество. Мы никогда не гонялись за “хайпом” или модой. В нашей области - это лишь сигналы к изучению инструмента чуть ближе, но финальное решение всегда основано на прагматизме и трезвой оценке.
Конечно, даже сейчас, если начать искать что-то вроде “работа с состоянием в javascript” - в первую очередь полезут многочисленные ссылки на Redux, Input+Output, props+setState, модные потоки, Observable и RxJS и только где-то после этого всего можно найти сравнение Redux и MobX.
И это понятно, ведь мир веб разработки сейчас - это как дикий запад. Разработчики сходят с ума, переизобретают то, что уже работало в разработке интерфейсов лет 20 или больше. От части это связано с молодостью самого понятия “Приложение JavaScript”, с другой стороны - это обусловлено низким порогом входа в веб разработку и общей тенденцией “сделать свой фреймворк”.
Многие из разработчиков, которые работали с UI на таких платформах как WinForms, WPF, GTK, QT и даже Delphi, с лёгкостью ответят на вопрос о хранении состояния и о том, как интерфейс должен реагировать на изменения. Особенно в этом плане понятна и стабильна концепция Model-View-ViewModel с реализацией функции отслеживания изменения свойств (например, INotifyPropertyChanged
на платформе .NET). И в целом такие понятия как “Компоненты”, “Состояние”, “Входящие значения”, “События”, “Перерисовка” - всё это уже много лет стабильно используется для разработки интерфейсов.
Разные подходы к формированию интерфейсов
Однако, с появлением ReactJS и Angular всё чаще стали говорить о том, что формирование интерфейсов должно быть декларативным. Что гораздо проще и понятнее, когда можно использовать выражение UI = f(State)
и иметь чистый и всегда согласованный интерфейс. И с этим крайне тяжело спорить, ведь все те же разработчики настольных приложений знают к чему приводит возможность императивной манипуляции компонентами UI напрямую и ручная синхронизация с состоянием: артефакты, поломанные сценарии, недовольные пользователи. Этот недостаток гораздо меньше проявляется в платформе WPF, т.к. там как раз объявление интерфейса через разметку XAML является крайней формой декларативности, а модель представления нужна только для наполнения данными. Вообще, я думаю, что разработчики Angular довольно грубо взяли на вооружение именно этот подход (по крайней мере они попытались).
С другой стороны React с его функцией render()
и чистыми компонентами-функциями никогда не называл себя декларативным, скорее функциональным. И вот тут, как раз, скрыта самая интересная деталь. Ведь то, что является функциональным (в чистом виде, как функция без побочных эффектов) можно с большой лёгкостью превратить в декларативное, но не наоборот.
Нам важно было сохранить простоту и скорость разработки интерфейсов, при этом обязательно учитывая эти разные подходы и их особенности. И, конечно, никто не хотел давать разработчикам лёгкую возможность испортить интерфейс кучей императивного кода.
Так почему же не Redux?
Итак, к началу нашего сравнения мы подошли с разными вариантами UI реализации, но при этом все в один голос говорили о том, что обработка состояния на клиенте должна быть строгой, отделённой от UI логики и изолированной в одном месте (если говорить про компонент). Кажется, тут идеально подходил Redux с его архитектурой FLUX и изоляцией действий и редьюсеров. Но было у него уже в тот момент пара недостатков:
- Он требовал написания большого количества дублирующего кода
- При композиции разных компонентов и наивной реализации глобальное состояние разрасталось до огромных размеров и без сложных оптимизаций уже невозможно было что-то делать
Если первый минус можно было решить сравнительно просто, введя небольшую абстракцию классов-команд, то вот уже со второй проблемой просто и понятного решения, которое бы позволило нам расширять набор компонентов и сами интерфейсы динамически, мы не нашли.
Почему mobx-state-tree лучше чистого MobX?
Где-то как раз в тот период (с апреля по май 2017) я наткнулся на статью про mobx-state-tree. Мне показалась эта концепция крайне интересной. Этот подход позволял выделить отдельно ViewModel и рисовать View не задумываясь о лишних или принудительных перерисовках. При этом мы могли в довесок получить:
- Строгость изоляции изменений состояния внутри действий
- Типизация и структурирование моделей (поддержка TypeScript на высоте)
- Транзакционность в части перерисовок по результатам изменений
- История изменения модели (машина времени)
- Простое API для мутаций объектов внутри действий
По сравнению с чистым MobX мы могли получить реальную строгость в части связывания расположения данных и действий, манипулирующих этими данными. В целом - это было идеальное решение. Мы разобрали несколько сценариев. Я попробовал собрать несколько прототипов с использованием этой библиотеки. Всё было идеально. Решение было принято и концептуально мы планировали использовать именно MST для дальнейшей реализации прототипов и оценки разных фреймворков.
Почему в итоге MobX оказался лучше, чем mobx-state-tree?
Забегая немного вперёд, хочу сказать, что для нас чистый MobX оказался предпочтительнее более строгого MST. Причин тому несколько. Во-первых, мы быстро поняли, что в условиях расширяемой компонентной модели нам будет довольно сложно стыковать независимые части состояния друг с другом, оставаясь в рамках заданных авторами MST. А во-вторых, как и в Redux тут используется подход единого дерева модели, что опять же ограничивает нас в части расширяемости и простоты разработки. И, конечно, мы бы не были кровавым энтерпрайзом, если бы не изобрели своё решение, избавленное от фатального недостатка. 😉
Что дальше?
Хочу добавить, что уже на момент написания этой заметки многое в mobx-state-tree изменилось в лучшую сторону. Стало доступно много интересных концепций и быстродействие выросло на порядки. И мы по-прежнему посматриваем на то, как развивается сообщество и какие происходят изменения в мире веб разработки.
В следующих частях я планирую рассказать уже детально о ходе нашего эксперимента. Сделаю первые выводы по итогу первой фазы сравнения. Ну и добавлю немного сумасшествия в наш эксперимент (да были и весёлые и дикие моменты во всей этой рутине).