Всем привет!
Хочу поделиться своим опытом и инструментами, которые я использовал для миграции проекта с Angular 1 на React.
TLTR: Я написал модуль, с помощью которого можно трасформировать Angular компоненты (контроллер + шаблон) в React компоненты.
Не холивара ради
В данной статье я не буду доказывать почему и какой фреймворк лучше. Да, кроме React есть Vue, уже вышел Angular 6, а еще Ember, Svelte и многие другие… В общем, хочу рассказать, как я решал поставленную задачу, надеюсь мой опыт и наработки кому-то пригодятся.
Проект
У каждого могут быть свои причины перехода на другой фреймворк/библиотеку. В моей компании основной проект был написан когда React еще пешком под стол ходил довольно давно, для этого был выбран Angular 1.x. Иногда он приносил боль (дайджест цикл, магия с вотчерами и ангуляровскими промисами), но в целом дело свое делал.
Во всех новых смежных проектах, в том числе и в мобильной версии основного проекта, используется связка Redux + React + Typescript + CSS Modules. В итоге появилась своя библиотека компонентов, стилей, все проекты строго стандартизированы, разработка новых компонентов и подпроектов ускорилась в разы.
Основной проект продолжал жить параллельно на Angular и требовал на поддержку всё больше и больше времени, потому что нельзя просто так взять и использовать готовый код приходилось решать заново уже решённые задачи, писать с нуля компоненты. Тем более на горизонте появилась перспектива объединить основную и мобильную версии проекта в один проект с адаптивной вёрсткой.
Да, есть ngReact, но превратить проект в этакого монстра Франкенштейна не было особого желания. Поэтому было принятно решение перенести проект на React для упрощения его развития и поддержки.
Что было
Основной проект
- Проект на Angular 1.4.10, angular-ui-router, angular-ui (модальные окна, календари)
- 60/40 — Typescript/ES2015+
- 182 шаблона, 156 директив, 100 контроллеров
- angular-mock + Jest для тестирования
- LESS (включая LESS код из Bootstrap 3) + BEM
Смежные проекты и мобильная версия
- React 15.x (Preact.js), React-router
- 100% Typescript
- Jest для тестирования
- Библиотека компонентов + CSS модули
Отмечу, что большая часть всей бизнес-логики (валидаторы, отправка запросов, утилиты и т.д.) была реализована отдельно на Typescript в виде NPM-модулей, что позволяет легко переиспользовать код между проектами независимо от фреймворка.
Берёмся за дело
"Я люблю рутину и рефакторинг!" — ни один разработчик на свете
Я думаю, многие согласятся, что рефакторинг это не самое интересное занятие. Поэтому я решил частично автоматизировать этот процесс.
Даже поверхностно сравнив компоненты React и Angular, можно вывести (конечно сильно упрощённую) формулу:
React.Component = Angular Controller + Angular Directive + Angular Template;
Так получился ng2react-builder
ng2react-builder
Вы, наверное, уже поняли, что я мастер давать названия модулям. Ну ладно, не об этом...
Что умеет модуль
Лучше всего посмотреть пример из документации, а еще лучше примеры компонентов в тест-кейсах.
Мы можем скормить модулю наш шаблон и контроллер (директивы пока в пролёте), и на выходе получится собранный React компонент (React.PureComponent или React.Component) с JSX разметкой.
Без контроллера можем легко собрать простой компонент без состояния (React.StatelessComponent).
ng2react-builder Пытается преобразовать все Angular-выражения в валидные JS/JSX конструкции:
- из
ng-repeat
мы получим нативный JS'ный.map()
с JSX выводом
<!-- было -->
<div>
<span ng-repeat="item in list as | limitTo:5 as results">{{item.name}}</span>
</div>
<!-- стало -->
<div>
{results.slice(0, 5).map((item, index) => {
return <span key={`child-${ index }`}>{item.name}</span>
})}
</div>
- контент
{{expression}}
будет преобразован в{expression}
- трансформация обработчиков событий и нестандартных директив:
<!-- было -->
<a ng-click="$event.preventDefault(); selectItem(item)">{{item.name}}</a>
<my-icon="calendar"><my-icon/>
// часть настроек для ng2rect-builder'а
directivesToTags: {
'my-icon': {
tagName: 'MyReactIcon',
valueProp: 'type'
}
}
<!-- стало -->
<a onClick={(event) => {
event.preventDefault();
selectItem(item);
}}>
{item.name}
</a>
<MyReactIcon type="calendar"/>
- и много другого. Советую посмотреть примеры и API!
Ещё особо полезной вещью может стать возможность преобразования директив в вызов JS функции (сейчас поддерживаются только директивы, заданные как атрибут):
<!-- было -->
<span my-directive="some.value"></span>
// часть настроек для ng2rect-builder'а
directivesToTextNodes: {
myDirective: {
callee: 'myFunc',
calleeArguments: ['arg1']
}
}
<!-- стало -->
<span>{myFunc(arg1, 'some.value')}</span>
Как это работает
- Шаблон (если он задан) разбирается через parse5, дальше работаем с HTML AST.
- Проводим всевозможные нормализации и трансформации директив, выражений, обработчиков и т.д.
- AST собирается в JSX шаблон
- Контроллер (если он задан) разбирается на AST с помощью TypeScript Compiler API.
- Контроллер преобразовывается в класс компонента, добавляется метод
render
с полученным JSX шаблоном - Общий код компонента прогоняется через Prettier
- Готово! Берём напильник...
Почему Typescript Compiler API
- Контроллер может быть написан на TypeScript
- Мы хотим получить на выходе React компонент на TypeScript с сгенерированными интерфейсами State и Props.
Чудес не бывает
Увы. Как я не пытался.
Как я написал выше, придётся брать напильник и продолжать рефакторинг вручную, но этот модуль сэкономил мне огромную часть времени на рутинных задачах, особенно в переводе Angular шаблонов на JSX.
Что стало
- Основной проект полностью переписан на Typescript и React
- Стало легче переиспользовать компоненты и код с остальными проектами
- Рефакторинг/написание новых тестов, да помогут нам Jest snapshot'ы :)
Что впереди
- Переход с LESS + BEM на CSS-модули
Надеюсь, вам понравилось, и я помог кому-то сэкономить время в процессе рефакторинга ;)
Автор: webschik