Pull to refresh

Как быстро и качественно импортозаместить UI

Level of difficultyMedium
Reading time8 min
Views4K

Всем привет. ( Поправил статью, постарался учесть комментарии.)

Хочу рассказать о своем опыте импортозамещения UI.

Не предлагаю ничего нового, просто известный подход, приложенный к конкретной ситуации.

Совсем кратко задачу можно описать так:

Есть: Исходный проект с множеством таблиц и форм.

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

Решение:

  1. Автоматически собрать необходимую информацию из исходного проекта в файл

  2. Дополнить этот файл в той части которую не получилось собрать автоматически

  3. Cоздавать нужные файлы по шаблону заменяя подстановочные символы в шаблоне значениями из файла. Полученные файлы могут быть если необходимо доработаны

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

Очевидные для меня плюсы такого подхода:

  1. На порядок быстрее

  2. Код максимально однообразен

  3. Код может быть покрыт автоматически созданными тестами

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

Теперь подробнее.

Я несколько лет участвовал в разработке Web приложения на net framework 4.7 слой UI которого построен на Devexpress. В один прекрасный момент поступила команда импортозаместить приложение. Собственно, импортозамещение можно разделить на 3 части.

  1. Смена базы (отдельная задача, сейчас не о ней)

  2. Смена framework 4.7 на .NET (выполняется достаточно просто с помощью upgrade-assistant)

  3. Смена UI (нужно уйти от Devexpress, так как текущая версия не поддерживает Core, а новую увы нам не продадут)

UI в новом приложении решено было сделать на MVC, в для того что бы уменьшить различия между новым и старым кодом. По крайней мере контроллеры и расположение файлов cshtml остается прежним. Это значительно упростило мою задачу. В качестве нового слоя UI было принято решение использовать чистый Vue + библиотека для отображения таблиц. В целях упрощения отказались от идеи использовать SPA. Каждая странница должна загружаться отдельно. Так же в целях упрощения было принято решение отказаться от сборщиков и загружать js библиотеки через cdn. (о плюсах и минусах такого решения рад буду услышать в комментариях.)

 Значительная часть нашего исходного Web приложения — это табличные формы и относительно простые карточки к ним. Их много, более 70, написаны в разное время и разными людьми, и естественно по-разному, что в них точно одинаково так это использование таблиц Devexpress.

 Что бы сделать новое приложение максимально однообразным мне было поручено разработать образцы кода, которые будет использоваться в новом приложении. Эти образцы были согласованы со всеми разработчиками.

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

Стенд (прототип) на котором я обкатывал это решение я опубликовал на GitHub:https://github.com/SergiyShest/DevExpressToDevExtremeMigrate .

На всякий случай скажу, что весь этот код написан в личное время по личной инициативе и не является ни чьей собственностью кроме меня. Шаблоны, по которым сделано новое приложение так же отличаются. (К примеру, пришлось использовать Vue 2 так, как в компании могут использоваться браузеры, не поддерживающие JavaScript module).

В прототипе 3 приложения:

  1. Source: Файлы исходного приложения на net framework 4.7 + MVC и Devexpress.Приложение не собирается потому, что я удалил все лишнее и оставил только необходимые файлы. Когда-то я нашел это приложение как пример для изучения Devexpress.

  2. Target: Целевое приложение на .NET MVC. Изначально создано по шаблону в VisualStudio.  В него добавлен проект Core в котором в папке Entity помещены те же файлы Entity что и в исходном приложении. Что бы в не заморачиваться с базой данных там же расположен класс TestDataHelper который возвращает случайным образом заполненный IQueryable<T> имитируя работу с базой.  В проект UI добавлены базовые Generic контроллеры и необходимые js файлы.

  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). Счел более удобным не сохранять информацию о колонках и полях формы, расположении файлов и. т. п. Эту информацию редактировать мне не возникла необходимость, без нее collector.json получается более кратким.

5.      Дополнить или отредактировать собранную информацию.

collector.json до редактирования.
collector.json до редактирования.

a)       Так как из-за разнородности исходного кода не для всех значений можно получить правильные названия модели и т.п., то файл нужно дополнить руками. К примеру CardEntityName всегда соответствует Entity табличной формы что всегда не так. Не найдены заголовки форм (карточек в нашей терминологии)

b)      Объекты в файле json для которых нет необходимости в автогенерации нужно удалить(закомментировать) или установить признак AlwaysSkip.

c)       Отредактированный файл нужно разместить в СodeGenerator\templates\collector.json (Я специально создаю его в другом месте, что бы исключить возможность перезатереть при запуске тестов

6.      Получить результирующий код выполнив тест GeneratorCommand.GenerateAll() логика работы приблизительно следующая:

a)       Читается файл collector.json.

b)      По каждому объекту в файле собирается дополнительная информация (состав колонок и поля формы)

c)       Код нужных классов создается по шаблону. На каждый объект следующие классы:

i)        Контроллер табличной формы

ii)       Вью табличной формы  

iii)     Контроллер формы

iv)     Вью формы

v)       Js тест

vi)     Для удобства отладки формируется так же меню.

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

 Файлы контроллеров сохраняются в том же месте и с тем же именем как в исходном приложении, Файлы представлений всегда Index.cshtml. (спасибо, что мы выбрали MVC)

  7.      Отладка приложения. Здесь все стандартно.

8.      И наконец моя любимая тема, так сказать вишенка на торте ТЕСТЫ.  

Для тестирования я решил использовать Cypress (чем мне нравится cypress так тем, что дает стабильные результаты).

В демонстрационном примере приведены простейшие тесты на табличную форму своего рода Smoke Tests. Тесты формируется по шаблону.

describe('Тест журнала $MainHeader$', () => {

 it('Проверка наличия заголовка', () => {
     cy.visit('https://localhost:7210/$HtmlRequestPath$')
    cy.contains('H1', '$MainHeader$')
    
  })  
  
  it('проверка фильра на наличие записей', () => {
      cy.visit('https://localhost:7210/$HtmlRequestPath$')

    cy.get('#d1From').type('1023-12-20').blur();
    cy.get('#d1To').type('2024-12-31').blur();
    cy.get('#findButton').click();
    cy.wait(1000);

    cy.get('.dx-info').invoke('text').then((text) => {
      // регулярное выражение для извлечения числа из текста Page 1 of 1 (0 items)
      const matches = text.match(/\((\d+)/);
      
     // Проверяем, что удалось извлечь число и оно больше 0
     const extractedNumber = matches && parseInt(matches[1], 10);
     cy.wrap(extractedNumber).should('be.gt', 0);
    })
   
  })

  it('Проверка фильтра на отсутствие записей', () => {
      cy.visit('https://localhost:7210/$HtmlRequestPath$')
    cy.get('#d1From').type('2000-12-31').blur();//unreal date tnere no records
    cy.get('#d1To').type('2000-12-31').blur();
    cy.get('#findButton').click();
    cy.wait(1000);
    cy.get('.dx-info').invoke('text').then((text) => {
      //регулярное выражение для извлечения числа из текста Page 1 of 1 (0 items)
      const matches = text.match(/\((\d+)/);
      
     // Проверяем, что удалось извлечь число и оно 0
     const extractedNumber = matches && parseInt(matches[1], 10);
     cy.wrap(extractedNumber).should('eq', 0);
    })
  })
})

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

Тесты можно запустить из Visual Studio через Test Explorer. Выглядит это так.

Запуск  тестов в VisualStudio
Запуск тестов в VisualStudio

В процессе отладки я запускал тесты непосредственно через Cypress. На каком-то этапе это необходимо, но мне не нравится необходимость менять контекст (инструмент) в процессе работы.  Кроме того, запуск тестов из Visual Studio через механизм тестов теоретически дает возможность провести какие-то манипуляции с базой, или что значительно красивее запустить приложение с специальным конфигом что бы загрузился мокнутый адаптер работы с базой.  

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

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

Еще раз напоминаю что весь код доступен по адресу https://github.com/SergiyShest/DevExpressToDevExtremeMigrate

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

 

 

 

 

 

Tags:
Hubs:
Total votes 12: ↑0 and ↓12-12
Comments26

Articles