Як мігрувати додаток з AngularJS на React і Redux

18

Від автора: з цього року я працюю в 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 і імпортували в нього всі залежності:

require(‘angular’);
require(‘angular-resource’);
// …other dependencies

Велика частина залежностей реєструється сама глобально в об’єкті window при імпорті. Нам залишилося лише імпортувати їх, як показано вище. Частину доведеться імпортувати вручну. Нижче наведено приклад того, що нам треба було зробити з moment і jQuery:

window.moment = require(‘moment’);
window.$ = require(‘jquery’);
window.jquery = window.$;
window.jQuery = window.$;

Це може здатися дивним, але нам потрібно врахувати, що більша частина залежностей покладається на window.$, інша частина на window.jQuery, решта на window.jquery.

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

require(‘./vendors’);

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

require(‘./vendors’);
require(‘./app/common/index’);
require(‘./app/core/index’);
require(‘./app/layout/index’);

Якщо у вас немає цих індексів, можете спробувати інше рішення (яке не часто радять). Необхідно знайти регулярний вираз, яке буде задовольняти всім файлам і буде імпортувати їх через require.context:

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:

$ npm i —save ngreact

Після чого імпортувати його в вендори:

require(‘ngreact’);

Також необхідно встановити React і react-dom в проект:

npm i —save react react-dom

після чого зареєструвати модуль react в Angular:

angular.module(‘app’, [‘react’]);

Після цього можна створити компонент button, як ми це робимо в додатках React:

import React from ‘react’;
const Button = ({ children, …restProps }) => (
{children}
);
export default Button;

Далі визначаємо директиву, яка буде виступати обгорткою для button:

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:

import reactButton from ‘path/to/react-button’;
angular
.module(‘app’)
.directive(‘reactButton’, reactButton);

Які модулі Angular ви будете використовувати для реєстрації директиви не важливо. Головне не забудьте зареєструвати директиву в додатку.

Після реєстрації директиву можна використовувати в будь-якому поданні Angular. Приклад:

Зауважте, що замість CamelCase ми використовуємо тирі для розбиття слів. reactButton перетворюється в react-button, а className стає class-name. Важливо пам’ятати це, так як це поширена помилка, яка може відібрати кілька годин дебага.

ngReact часто використовується для фонового невеликих компонентів у додатках AngularJS, однак він не продуктивний.

Angular UI Router дозволяє передавати шаблон параметрів в рауса конфіг. Знаючи це, можна створити компонент-обгортку для всіх екранів програми, після чого використовувати ці обгортки:

$stateProvider.state(‘user.login’, {
url: ‘/login’,
template: ‘</’,
});

У наведеному вище прикладі ми задаємо рауса login і передаємо його компонент, який і є цілим екраном login. Так ми можемо мігрувати цілий екран за один раз замість міграції компонентів окремо.

Мій улюблений рада – встановіть Storybook в проект для створення і тестування маленьких компонентів. Так легше створювати надійні компоненти і об’єднувати їх в екрани. Екрани: або сторінки – кореневої компонент роута.

Спільне використання залежностей

Ми можемо задати весь екран, це дивно. Однак, дійшовши до цього моменту, нам також необхідно поділитися деякими Angular залежностями з React.

У випадку з BEN необхідні нам залежності були готові тільки після ініціалізації Angular і виконання провайдерів, конфига і т. д. Враховуючи це, ми не змогли експортувати їх за допомогою ключового слова export. Для вирішення це завдання ми створили об’єкт і хелпер функцію для вставки залежностей. Для цього необхідно створити файл ngDeps.js код:

export const ngDeps = {};
export function injectNgDeps(deps) {
Object.assign(ngDeps, deps);
};
export default ngDeps;

Ми викликаємо injectNgDeps всередині Angular run, як на прикладі нижче:

import { injectNgDeps } from ‘path/to/ngDeps’;
angular
.module(‘app’, [])
.run([
‘$rootScope’,
‘$state’,
($rootScope, $state) => {
injectNgDeps({ $rootScope, $state });
},
]);

Це потрібно для того, щоб у нас був доступ до залежностей в максимально короткі терміни, а також щоб run був одним з перших виконаних процесів в ініціалізації. injectNgDeps як аргумент приймає об’єкт і зливає його з об’єктом ngDeps.

Коли вам знадобляться залежності в компоненті React, вам потрібно зробити наступне:

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

}
}

Зауважте, що першою справою ми імпортуємо ngDeps. Якщо спробувати отримати доступ до ngDeps.$state відразу після import, ви отримаєте undefined, так як процес run ще не запущений. Тому ми отримуємо значення всередині методу contructor компонента, так як на компоненти будуть створені примірники тільки після ініціалізації Angular.

Залежно витягуються з ngDeps, і ми призначаємо їх у об’єкт this, так як таким чином ми можемо отримати this.$state всередині будь-якого методу класу.

Так ми можемо ділитися будь-якою залежністю Angular з компонентами React. Але не використовуйте часто ngDeps. Завжди думайте: чи можна експортувати цю залежність через export? Якщо так, використовуйте експорт, а не ngDeps.

Також важливо підкреслити, що необхідно обмежити доступ до ngDeps тільки для верхніх компонентів в дереві (тобто екрани і, можливо, пара контейнерів). Нижче можна передавати через властивості. Так буде легше видалити ngDeps, коли прийде час.

Інтеграція Redux в додаток

Після вирішення питання з спільним використанням залежностей ми можемо перейти до інтеграції Redux в додаток. Зробити це нескладно. Однак є кілька нюансів.

По-перше, налаштуйте store по інструкції, як в будь-якому додатку. Після створення об’єкта store його необхідно експортувати:

export const store = createStore(rootReducer);

Так ми зможемо отримувати об’єкт store в інших файлах у додатку.

У звичайному додатку контейнери інтегруються в store за допомогою методу connect з react-redux. Однак це працює тільки тому, що ми вставляємо Provider з store як кореневої компонент у додатку:

ReactDOM.render(
,
rootEl
)

Проблема в тому, що у нас не один кореневий компонент в додатку, а багато. Контролювати де повинен бути Provider, а де немає вручну не практично. Тому ми створили High Order Component, який абстрагує логіку і вставляє Provider у вигляді обгортки при необхідності. Я опублікував його на Github і NPM як redux-connect-standalone.

Для установки в NPM

npm i —save redux-connect-standalone

Після цього можна створити файл connect:

import createConnect from ‘redux-connect-standalone’;
import store ‘path/to/youStore’;
export const connect = createConnect(store);

В компонентах замість імпорту методу connect з react-redux імпортуйте його з файлу. І використовуйте його точно так само, як з оригінальним методом:

import { connect } from ‘path/to/youConnect’;
export default connect(
mapStateToProps,
mapDispatchToProps
)(YourContainer);

Так як ми залишаємо підпис вихідного методу, то коли кореневим компонентом програми стане Provider, вам потрібно буде лише виконати пошук і заміну в методі import:

import { connect } from ‘react-redux’;

«Якщо ви використовуєте або хочете використовувати redux-form в додатку, я також створив і опублікував HOC на метод reduxForm, redux-form-connect-standalone. Він використовується так само, як HOC вище.»

Заключні слова

Знаючи ці рецепти, ви зможете поступово мігрувати свій додаток. Проте завжди є інші складні моменти, які спливають під час міграції базової технології програми. Важливо пам’ятати, що всі рішення вище це щось середнє між Angular і React. Кінцева мета – позбутися всіх і використовувати оголошення React і Redux. При створенні свого рішення подумайте про те, наскільки складно його потім можна видалити.