Всем привет!


Хочу поделиться своим опытом и инструментами, которые я использовал для миграции проекта с 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"/>


Ещё особо полезной вещью может стать возможность преобразования директив в вызов 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


  1. Контроллер может быть написан на TypeScript
  2. Мы хотим получить на выходе React компонент на TypeScript с сгенерированными интерфейсами State и Props.

Чудес не бывает


Увы. Как я не пытался.


Как я написал выше, придётся брать напильник и продолжать рефакторинг вручную, но этот модуль сэкономил мне огромную часть времени на рутинных задачах, особенно в переводе Angular шаблонов на JSX.


Что стало


  • Основной проект полностью переписан на Typescript и React
  • Стало легче переиспользовать компоненты и код с остальными проектами
  • Рефакторинг/написание новых тестов, да помогут нам Jest snapshot'ы :)

Что впереди


  • Переход с LESS + BEM на CSS-модули

Надеюсь, вам понравилось, и я помог кому-то сэкономить время в процессе рефакторинга ;)

Комментарии (6)


  1. Drag13
    31.05.2018 11:59

    Спасибо за статью, сразу вопрос:

    Переход с LESS + BEM на CSS-модули


    А как вы планируете работать с переменными в css модулях — общими отступами, цветами, размерами шрифта?


    1. webschik Автор
      31.05.2018 12:06
      +1

      Мы используем CSS переменные


      1. Drag13
        31.05.2018 12:32

        Спасибо, посмотрю!


        1. webschik Автор
          31.05.2018 13:22

          Советую особенно обратить внимание на различия между нативными переменными и препроцессорными


      1. witka
        02.06.2018 16:56

        а с поддержкой как быть? тот же edge не всех версий поддерживает css variables?


        1. webschik Автор
          02.06.2018 23:06

          Если нужно поддерживать не самые последние версии браузеров, можно использовать PostCSS, но тогда css variables будут выступать в роли обычных переменных из препроцессоров, без своих вкусностей вроде runtime обновления из media-запросов и доступа из JS.