От автора: с этого года я работаю в BEN Group. Моя основная задача – помогать переносить с AngularJS приложение на React и Redux. В проекте мы создали решения, которые замечательно работают. В этой статье я покажу вам основные подходы, которых мы придерживались, а также поделюсь созданными нами решениями. Это поможет нам постепенно мигрировать проект и не потерять голову. Дисклеймер: наша задача – не рефакторинг старого кода, а максимальное его удаление. Мы не долгими путями или путями, которые меняют старый код, чтобы сделать его «лучше». Мы просто напишем новый качественный код.
Перемещение билда на Webpack
Этот шаг, на мой взгляд, самый важный во всем процессе. В Webpack можно начать с инструкции import для подключения зависимостей и модулей и избавления от Dependency Injection(DI). Webpack также обязателен для написания React кода в приложении.
Если мы используете кэш шаблонов Angular, Pug (Jade) или другие инструменты, влияющие на билд, не беспокойтесь, в Webpack есть загрузчик для всех этих инструментов. Не забудьте позволить настройку Webpack для транспиллинга ES2015 и JSX.
В этом шаге мы не будем перемещать все DI в импорты, а заставим наш билд работать с Webpack. Важно помнить это, чтобы не застрять с этой задачей на несколько недель и вызвать конфликты в дюжине файлов.
В процессе билда AngularJS берет все зависимости из node_modules и помещает их в бандл. Нам необходимо сохранить это поведение в новом билде.
Смотрите на старый код, как на врага, которого нужно уничтожить. Нужно действовать с осторожностью и стратегически. Это значит, что иногда нам придется делать то, что нам не нравится.
Для решения этого вопроса мы создали файл vendor.js, и импортировали в него все зависимости:
1 2 3 |
require('angular'); require('angular-resource'); // ...other dependencies |
Большая часть зависимостей регистрируется сама глобально в объекте window при импорте. Нам осталось лишь импортировать их, как показано выше. Часть придется импортировать вручную. Ниже приведен пример того, что нам нужно было сделать с moment и jQuery:
1 2 3 4 |
window.moment = require('moment'); window.$ = require('jquery'); window.jquery = window.$; window.jQuery = window.$; |
Это может показаться странным, но нам нужно учесть, что большая часть зависимостей полагается на window.$, другая часть на window.jQuery, остальные на window.jquery.
После создания вендорного файла импортируйте его в точку входа в приложение, это подключит в бандл все зависимости:
1 |
require('./vendors'); |
Следующий шаг – убедиться, что все файлы приложения попали в бандл. В идеале, на каждый модуль должен быть свой файл index, импорты контроллеров, фабрики, представления и т.д. Имея все это, вам останется импортировать эти индексы в точку входа в приложения, как было с вендорами. Пример ниже:
1 2 3 4 |
require('./vendors'); require('./app/common/index'); require('./app/core/index'); require('./app/layout/index'); |
Если у вас нет этих индексов, можете попробовать другое решение (которое не часто советуют). Необходимо найти регулярное выражение, которое будет удовлетворять всем файлам и будет импортировать их через require.context:
1 2 3 4 |
function requireAll(r) { r.keys().forEach(r); } requireAll(require.context('./app/', true, /\.(js|jsx)$/)); |
Код выше заставит Webpack подключить в бандл все файлы .js и .jsx из папки /app и ее дочерних папок. Если хотите пойти этим путем, не забудьте, что у вас могут быть файлы .test.js ,.spec.js и даже .stories.js – их нужно будет исключить из регулярного выражения.
Не забывайте, что в некоторых случаях Angular полагается на порядок загрузки файлов, поэтому это решение может вообще сломаться.
После того как поднимите билд сразу же создайте пулл запрос из мастер ветки. Без React перенос билда на Webpack – это уже плюс для приложения. Angular DI сильно связывает приложение, и Webpack помогает нам решить эту задачу.
Рендер компонентов React в AngularJS
Второй важнейший шаг, так как без него невозможно провести постепенную миграцию. Идея в том, что компоненты React можно использовать в Angular, как директивы. Для этого в нашем проекте мы используем ngReact.
«Репозиторий ngReact советует использовать библиотеку react2Angular. Однако мы используем Angular 1.5.8 в нашем приложении. При попытке использовать другую библиотеку у нас возникли проблемы. В другом проекте я уже использовал react2Angular, который использовал более раннюю версию Angular, и у меня не возникало проблем. ngReact больше вообще не обновляется, но в нем есть все функции, необходимые нам для трансформации компонентов в директивы. Мой совет – выберите работающую библиотеку и придерживайтесь ее (они обе похожи).»
Для интеграции ngReact в проект его необходимо установить через npm:
1 |
$ npm i --save ngreact |
После чего импортировать его в вендоры:
1 |
require('ngreact'); |
Также необходимо установить React и react-dom в проект:
1 |
npm i --save react react-dom |
после чего зарегистрировать модуль react в Angular:
1 |
angular.module('app', ['react']); |
После этого можно создать компонент button, как мы это делаем в приложениях React:
1 2 3 4 5 6 7 |
import React from 'react'; const Button = ({ children, ...restProps }) => ( <button {...restProps}>{children}</button> ); export default Button; |
Далее определяем директиву, которая будет выступать оберткой для button:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import Button from 'path/to/Button'; const props = [ 'children', 'id', 'className', 'disabled', 'etc..', ]; const ReactButton = reactDirective => reactDirective(Button, props); ReactButton.$inject = ['reactDirective']; export default ReactButton; |
В файле директивы мы должны задать имя всех свойств, которые используются в button, чтобы ngReact понимал, что передавать в компонент. Директива объявлена, необходимо зарегистрировать его в Angular:
1 2 3 4 5 6 7 |
import reactButton from 'path/to/react-button'; angular .module('app') .directive(‘reactButton’, reactButton); |
Какие модули Angular вы будете использовать для регистрации директивы не важно. Главное не забудьте зарегистрировать директиву в приложении.
После регистрации директиву можно использовать в любом представлении Angular. Пример:
1 2 3 |
<div> <react-button class-name="btn"></react-button> </div> |
Заметьте, что вместо CamelCase мы используем тире для разбиения слов. reactButton превращается в react-button, а className становится class-name. Важно помнить это, так как это распространенная ошибка, которая может отнять несколько часов дебага.
ngReact часто используется для рендера небольших компонентов в приложениях AngularJS, однако он не продуктивен.
Angular UI Router позволяет передавать шаблон параметров в роут конфиг. Зная это, можно создать компонент-обертку для всех экранов приложения, после чего использовать эти обертки:
1 2 3 4 5 6 7 |
$stateProvider.state('user.login', { url: '/login', template: '<react-screen-login></<react-screen-login>', }); |
В примере выше мы задаем роут login и передаем его в компонент, который и является целым экраном login. Так мы можем мигрировать целый экран за один раз вместо миграции компонентов по отдельности.
Мой любимый совет – установите Storybook в проект для создания и тестирования маленьких компонентов. Так легче создавать надежные компоненты и объединять их в экраны. Экраны: или страницы – корневой компонент роута.
Совместное использование зависимостей
Мы можем задать весь экран, это удивительно. Однако дойдя до этого момента, нам также необходимо поделиться некоторыми Angular зависимостями с React.
В случае с BEN необходимые нам зависимости были готовы только после инициализации Angular и выполнения провайдеров, конфига и т.д. Учитывая это, мы не смогли экспортировать их с помощью ключевого слова export. Для решения это задачи мы создали объект и хелпер функцию для вставки зависимостей. Для этого необходимо создать файл ngDeps.js с кодом:
1 2 3 4 5 6 7 8 9 |
export const ngDeps = {}; export function injectNgDeps(deps) { Object.assign(ngDeps, deps); }; export default ngDeps; |
Мы вызываем injectNgDeps внутри Angular run, как на примере ниже:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
import { injectNgDeps } from 'path/to/ngDeps'; angular .module('app', []) .run([ '$rootScope', '$state', ($rootScope, $state) => { injectNgDeps({ $rootScope, $state }); }, ]); |
Это нужно для того, чтобы у нас был доступ к зависимостям в максимально короткие сроки, а также чтобы run был одним из первых выполненных процессов в инициализации. injectNgDeps в качестве аргумента принимает объект и сливает его с объектом ngDeps.
Когда вам понадобятся зависимости в компоненте React, вам нужно сделать следующее:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
import React, { Component } from 'react'; import ngDeps from 'path/to/ngDeps'; class Login extends Component { constructor(props) { super(props); const { $state, $rootScope } = ngDeps; this.$state = $state; this.$rootScope = $rootScope; } render() { return <div /> } } |
Заметьте, что первым делом мы импортируем ngDeps. Если попробовать получить доступ к ngDeps.$state сразу после import, вы получите undefined, так как процесс run еще не запущен. Поэтому мы получаем значение внутри метода contructor компонента, так как на компоненты будут созданы экземпляры только после инициализации Angular.
Зависимости извлекаются из ngDeps, и мы назначаем их в объект this, так как таким образом мы можем получить this.$state внутри любого метода класса.
Так мы можем делиться любой зависимостью Angular с компонентами React. Но не используйте часто ngDeps. Всегда думайте: можно ли экспортировать эту зависимость через export? Если да, используйте экспорт, а не ngDeps.
Также важно подчеркнуть, что необходимо ограничить доступ к ngDeps только для верхних компонентов в дереве (т.е. экраны и, возможно, пара контейнеров). Ниже можно передавать через свойства. Так будет легче удалить ngDeps, когда придет время.
Интеграция Redux в приложение
После решения вопроса с совместным использованием зависимостей мы можем перейти к интеграции Redux в приложение. Сделать это несложно. Однако есть несколько нюансов.
Во-первых, настройте store по инструкции, как в любом приложении. После создания объекта store его необходимо экспортировать:
1 |
export const store = createStore(rootReducer); |
Так мы сможем получать объект store в других файлах в приложении.
В обычном приложении контейнеры интегрируются в store с помощью метода connect из react-redux. Однако это работает только потому, что мы вставляем Provider со store как корневой компонент в приложении:
1 2 3 4 5 6 |
ReactDOM.render( <Provider store={store}> <MyAppRootComponent /> </Provider>, rootEl ) |
Проблема в том, что у нас не один корневой компонент в приложении, а много. Контролировать где должен быть Provider, а где нет вручную не практично. Поэтому мы создали High Order Component, который абстрагирует логику и вставляет Provider в виде обертки при необходимости. Я опубликовал его на Github и NPM как redux-connect-standalone.
Для установки в NPM
1 |
npm i --save redux-connect-standalone |
После этого можно создать файл connect:
1 2 3 4 |
import createConnect from 'redux-connect-standalone'; import store 'path/to/youStore'; export const connect = createConnect(store); |
В компонентах вместо импорта метода connect из react-redux импортируйте его из созданного файла. И используйте его точно так же, как с оригинальным методом:
1 2 3 4 5 6 |
import { connect } from 'path/to/youConnect'; export default connect( mapStateToProps, mapDispatchToProps )(YourContainer); |
Так как мы оставляем подпись исходного метода, то когда корневым компонентом приложения станет Provider, вам нужно будет всего лишь выполнить поиск и замену в методе import:
1 |
import { connect } from 'react-redux'; |
«Если вы используете или хотите использовать redux-form в приложении, я также создал и опубликовал HOC на метод reduxForm, redux-form-connect-standalone. Он используется так же, как HOC выше.»
Заключительные слова
Зная эти рецепты, вы сможете постепенно мигрировать свое приложение. Однако всегда есть другие сложные моменты, которые всплывают во время миграции базовой технологии приложения. Важно помнить, что все решения выше это что-то среднее между Angular и React. Конечная цель – избавиться от всех и использовать объявления React и Redux. При создании своего решения подумайте о том, насколько сложно его потом можно удалить.
Автор: Vinicius Dacal Lopes
Источник: //hackernoon.com/
Редакция: Команда webformyself.