Как быстро и качественно импортозаместить UI
Всем привет.
Хочу рассказать о своем опыте импортозамещения UI.
Не предлагаю ничего нового, просто известный подход, приложенный к конкретной ситуации.
Совсем кратко задачу можно описать так:
Есть: Исходный проект с множеством таблиц и форм.
Задача: Нужно в целевом проекте создать аналогичные таблицы и формы по другой технологии. По сути много однотипных классов и файлов, отличающихся заранее известными частями.
Решение:
1. Автоматически собрать необходимую информацию из исходного проекта в файл.
2. Дополнить этот файл в той части которую не получилось собрать автоматически.
3. Создавать нужные файлы по шаблону заменяя подстановочные символы в шаблоне значениями из файла. Полученные файлы могут быть если необходимо доработаны.
Первое что приходит на ум при решении такой задачи — это использовать встроенный в VisualStudio кодогенератор, он реализует похожую задачу. Я изначально рассматривал использование его. Но передумал. Получается много сложнее. Кроме того, генератор в студии предполагается использовать постоянно, мой генератор нужен одноразово что бы создать заготовку кода. Хотя я не исключаю, что буду применять его и далее.
Теперь подробнее.
Я несколько лет участвовал в разработке Web приложения на net framework 4.7 слой UI которого построен на Devexpress. В один прекрасный момент поступила команда импортозаместить приложение. Собственно, импортозамещение можно разделить на 3 части.
1. Смена базы (отдельная задача, сейчас не о ней)
2. Смена framework 4.7 на .NET (выполняется достаточно просто с помощью upgrade-assistant)
3. Смена UI (нужно уйти от Devexpress как вражеского продукта).
UI в новом приложении решено было сделать на MVC, в для того что бы уменьшить различия между новым и старым кодом. По крайней мере контроллеры и расположение файлов cshtml остается прежним. Это значительно упростило мою задачу. В качестве нового слоя UI было принято решение использовать чистый Vue + библиотека для отображения таблиц. В целях упрощения отказались от идеи использовать SPA. Каждая странница должна загружаться отдельно. Так же в целях упрощения было принято решение отказаться от сборщиков и загружать js библиотеки через cdn. (о плюсах и минусах такого решения рад буду услышать в комментариях.)
Значительная часть нашего исходного Web приложения — это табличные формы и относительно простые карточки к ним. Их много, более 70, написаны в разное время и разными людьми, и естественно по-разному, что в них точно одинаково так это использование таблиц Devexpress. Что бы избежать такого-же бардака в новом приложении мне было поручено разработать образец кода, который будет использоваться в новом приложении. Этот образец был согласован со всеми разрабами. При работе над образцом собственно и родилась идея создавать код автоматически. Автоматически созданный для табличной части код в 90% случаев может быть оставлен без изменения, изменения чаще всего касаются добавления кнопок полей в дополнительный фильтр. С формой редактирования сложнее. Я создаю заготовку с полями, которые найдены в модели. Тип полей и их взаимное расположение на форме необходимо дорабатывать.
Стенд (прототип) на котором я обкатывал это решение я опубликовал на GitHub SergiyShest/DevExpressToDevExtremeMigrate (github.com). На всякий случай скажу, что весь этот код написан в личное время по личной инициативе и не является ни чьей собственностью кроме меня. Шаблоны, по которым сделано новое приложение так же отличаются. (К примеру, пришлось использовать Vue 2 так, как в компании могут использоваться браузеры, не поддерживающие JavaScript module).
В прототипе 3 приложения:
1) Source: Файлы исходного приложения на net framework 4.7 + MVC и Devexpress.Приложение не собирается потому, что я удалил все лишнее и оставил только необходимые файлы. Когда-то я нашел это приложение как пример для изучения Devexpress.
2) Target: Целевое приложение на .NET MVC. Изначально создано по шаблону в VisualStudio. В него добавлен проект Core в котором в папке Entity помещены те же файлы Entity что и в исходном приложении. Что бы в не заморачиваться с базой данных там же расположен класс TestDataHelper который возвращает случайным образом заполненный IQueryable
3) CodeGenerator: Генератор который на основании Devexpress таблиц в исходном проекте создает аналогичные таблицы в целевом проекте, но уже по другой технологии (Devextreme). Шаблоны так же находятся в этом проекте.
Для ускорения разработки и гибкости генератор управляется запуском тестов. Это удобно для программ, которые выполняются только разработчиком.
Тесты для управления отделены от тестов, которые я использовал при разработке и вынесены в отдельный класс GeneratorCommand:
1) CollectInfo (): Изначальный сбор информации в файл
2) GenerateAll (): Генерация всего.
3) GeneratePart (): Генерация кода для отдельной таблицы.
Предусмотрены так же есть флаги, которые позволяют пропускать создание любого из файлов.
Предполагаемый алгоритм работы:
1) Разместить нужное исходное приложение на месте проекта Source или/и поправить пути в файлах CodeGeneration.cs и InfoCollector.cs.
2) Разместить целевое приложение на месте проекта Target или/и поправить пути в файлах CodeGeneration.cs и InfoCollector.cs.
3) В целевом приложении должны быть все Entity из исходного приложения по которым предполагается создавать формы.
4) Собрать данные запустив тест GeneratorCommand.CollectInfo () логика работы которого приблизительно следующая:
a) В исходном проекте UI ищутся все файлы по шаблону _*grid*.cshtml. В этих файлах содержится описание колонок таблицы и обычно есть модель, по которой можно понять какой класс Entity биндится с вьюхой или таблицей в базе данных. (иногда эту информацию приходится вытаскивать из контроллера).
b) Ищу класс Entity на для того что бы взять описания полей для формы редактирования (что бы поиск был быстрее необходимо обязательно установить свойство EntityPath в классе InfoCollector)
c) Сохраняю часть собранной информации в файл (collector.json).
5) Дополнить или отредактировать собранную информацию.
a) Так как из-за разнородности исходного кода не для всех значений можно получить правильные названия модели и т.п., то файл нужно дополнить руками.
b) Объекты в файле json для которых нет необходимости в автогенерации нужно удалить (закомментировать) или установить признак AlwaysSkip.
c) Отредактированный файл нужно разместить в СodeGenerator\templates\collector.json (Я специально создаю его в другом месте, что бы исключить возможность перезатереть при запуске тестов
6) Получить необходимый код выполнив команду GeneratorCommand.GenerateAll () логика работы которого приблизительно следующая:
a) Читается файл collector.json.
b) По каждому объекту в файле собирается дополнительная информация (состав колонок и поля формы)
c) Код нужных классов создается по шаблону. На каждый объект следующие классы:
i) Контроллер табличной формы
ii) Вью табличной формы
iii) Контроллер формы
iv) Вью формы
v) Js тест
vi) Для удобства отладки формируется так же меню.
d) Файлы сохраняются в том же месте и с тем же именем как в исходном приложении (спасибо, что мы выбрали MVC)
e) В случае необходимости предусмотрена возможность отключить генерацию любого из классов в списке выше.
7) Отладка приложения. Здесь все стандартно.
8) И наконец моя любимая тема, так сказать вишенка на торте ТЕСТЫ.
Для тестирования я использовал end to end тесты, написанные на Cypress (чем мне нравится cypress так тем, что дает стабильные результаты).
В демонстрационном примере простейшие тесты на табличную форму своего рода Smoke Tests.
· Проверка наличия заголовка
· Проверка что при пустых значениях фильтра в таблице есть данные
· Проверка, что, установив перекрестные даты в фильтре получаешь пустую таблицу
В логике тестов используется та особенность, что в шаблоне большинство таблиц имеет дополнительный фильтр по датам. Это позволяет задать значения фильтра таким образом, чтобы получить пустую таблицу. Для тех таблиц для которых даты нет, текст теста нужно поправить в процессе тестирования (по-хорошему нужно конечно автоматом создавать чуть другой тест для таких случаев, но руки не дошли).
Тесты можно запустить из Visual Studio через Test Explorer. Выглядит это так.
Запуск тестов в VisualStudio
В процессе отладки я запускал тесты непосредственно через Cypress. На каком-то этапе это необходимо, но мне не нравится необходимость менять контекст (инструмент) в процессе работы. Кроме того, запуск тестов из Visual Studio через механизм тестов теоретически дает возможность провести какие-то манипуляции с базой, или что значительно красивее запустить приложение с специальным конфигом что бы загрузился мокнутый адаптер работы с базой.
Для обеспечения возможности использовать NUnit для запуска js тестов пришлось написать не большой адаптер. Возможно здесь я изобрел велосипед, но велосипед простой, работает надежно.
В реальном приложении тесты написаны так же на карточки и пришлось учесть еще необходимость авторизации. Для авторизации я выбрал максимально простой подход, то есть перед каждым тестом поднимаю форму авторизации и ввожу данные тестового пользователя. С этой задачей Cypress справляется легко.