Всем привет!
Хочу поделиться своим опытом и инструментами, которые я использовал для миграции проекта с 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-модули
Надеюсь, вам понравилось, и я помог кому-то сэкономить время в процессе рефакторинга ;)
Drag13
Спасибо за статью, сразу вопрос:
А как вы планируете работать с переменными в css модулях — общими отступами, цветами, размерами шрифта?
webschik Автор
Мы используем CSS переменные
Drag13
Спасибо, посмотрю!
webschik Автор
Советую особенно обратить внимание на различия между нативными переменными и препроцессорными
witka
а с поддержкой как быть? тот же edge не всех версий поддерживает css variables?
webschik Автор
Если нужно поддерживать не самые последние версии браузеров, можно использовать PostCSS, но тогда css variables будут выступать в роли обычных переменных из препроцессоров, без своих вкусностей вроде runtime обновления из media-запросов и доступа из JS.