Коли Angular зустрічається з Redux

преамбула

Ми все більше чуємо про все більше використання концепції Redux з Angular (2+). Шукаючи кілька статей на цю тему, я зрозумів, що нашою прекрасною мовою Мольєра доступно занадто мало. Таким чином, мета цієї статті — допомогти вам зрозуміти Redux і його використання з фреймворком, який не є його улюбленою сферою. Якщо ви використовуєте Angular і не знайомі з цією концепцією, ви потрапили в потрібне місце.

передумови

Ця стаття призначена для розробників, які звикли використовувати Angular, не маючи чіткого уявлення про те, що таке Redux. Такі поняття, як Observables, Основи Typescript і Компоненти, будуть сприйматися як належне. Знання функціонального програмування буде перевагою.

Вступ

Redux — це архітектура, яка має на меті зосередити стан вашого додатка в одному місці. Дії будуть відправлятися подіями, які щоразу повертатимуться до попереднього стану, змінюватимуть його частину та повертатимуть новий, який буде використовуватися для керування вашими даними.
Не дуже зрозуміло? Не хвилюйтеся, мета цієї статті — демістифікувати, здавалося б, складне поняття, яке виявляється простим і логічним.
Перш ніж перейти до суті справи, я хотів би наполягати на тому, що Redux, який дуже часто використовується з React, жодним чином не залежить від останнього. Настав час зрозуміти переваги дуже практичної архітектури для використання з будь-яким компонентно-орієнтованим фреймворком/бібліотекою, і Angular є прекрасним прикладом.

Які проблеми з Angular можна вирішити за допомогою цього нового керування даними програми?

Змінні, спільні для різних компонентів різних рівнів

У компоненті Angular загалом рідко зустрічаються змінні readonly для чогось іншого, ніж константи служби або спостережувані. Змінні, які безпосередньо відображаються в компоненті, також змінюються цим самим компонентом. Але у вас може виникнути справжня проблема, коли ці змінні передаються як вхідні дані до підкомпоненту, і вони змінюються цим підкомпонентом. Це можливо, і це працює, тому що для цього створено прив’язку даних Angular. Проте перевірити це стає дуже важко, і код може мати побічні ефекти.
Уявіть таке дерево, як:

Компонент0 ├── Компонент1 └── Компонент2 └── Компонент3

У нас є змінна, яка є спільною для компонентів 1 і 3. Отже, ми повинні оголосити цю змінну як властивість Component0, лише щоб вона оновилася в Component2, а також у Component3. Нам доведеться помістити вихід у компонент 1, якщо ми хочемо змінити змінну після події. І зробіть те ж саме, не тільки в Component3, але і в Component2, щоб перейти до Component0 EventEmitter.

Багато коду, щоб просто змінити змінну, яка використовується в двох місцях на одній сторінці.

Очевидно, що Angular вже вбудовує рішення, які ми могли б використовувати в цьому випадку. Особливе використання сервісів, у яких ми будемо зберігати спільну змінну. Якби ми хотіли, щоб він був прив’язаний до обох компонентів, тобто зміни в реальному часі в Component1, коли він був змінений в Component3, то нам довелося б використовувати парадигми реактивного програмування, особливо Observables з RXJS. Це можливо, і багато програм працюють таким чином, але це може стати важко підтримувати, якщо шаблон повторюється регулярно в кількох місцях програми. Однак цю ідею не варто повністю виключати, оскільки вона стане основою для нашого рішення.

Зв’язані дані дуже нестабільні, і налагодження може бути складним

Дані можуть змінюватися весь час, і якщо ви постійно не працюєте над незмінністю, відстежувати зміни значень об’єкта може бути складно. Також важко зберігати історію дій і подій, які здійснюються на деякий час. Redux також пропонує рішення на цьому рівні за допомогою «налагодження подорожей у часі». Redux зберігає кожен раз, коли створюється новий стан, копію попередньої версії, і все доступне Інструменти розробника через розширення

Може бути важко організувати код, який змінює змінні чистим способом.

Тут ми дійсно переходимо до частини пояснення функціонального програмування, але рішенням, наданим цією парадигмою, є концепція «чистої функції». Чиста функція має один або кілька аргументів, не змінює жодну змінну за межами своєї області і повертає значення, яке завжди буде однаковим з тими самими аргументами. Це досить просто в теорії, і не завжди на практиці.
Якщо ви хочете глибше зануритися в пояснення та розуміння парадигми функціонального програмування, я можу лише порекомендувати статтю від Йоан Рібейру на цю тему.
Більше того, використання Angular часто змушує нас робити ООП, а не функціональне програмування, у тому сенсі, що ми змінюємо дані компонента (який є класом), які за визначенням є зовнішніми щодо методів складання.

Трохи теорії перед практикою

Тепер, коли ми підняли кілька моментів, притаманних розробці за допомогою Angular і які можна спростити за допомогою Redux, ми спробуємо добре зрозуміти шаблон, створивши базову програму Redux.

Іноді ви побачите Store, іноді State. Ці два слова в усьому світі охоплюють одне й те саме значення, а саме стан програми та її структуру даних.

Ця схема дуже проста. Від нашого компонента ми йдемо відправити дію. Ми можемо перекласти це простим фактом відправки виклику, події, яка сама викличе функцію.
Тоді ця дія буде застосовано до a редуктор хто буде діяти на були програми. Зрештою, цей самий стан буде зчитуватися компонентом(ами) в інтерфейсі користувача.
У двох реченнях і простій діаграмі ми могли б узагальнити концепцію досить легко. Але, вочевидь, свою роль відіграють і дії, і редуктори, і держава. І хоча може здатися, що коду та функцій багато для дрібниць, кожна з них має велике значення.
Як видно, потік повинен бути завжди односпрямований, і це важливий момент, який послужить наріжним каменем нашої архітектури. Дії користувача будуть проходити через компонент, який відправлятиме дії. Ці дії будуть відправлені в редуктори, наші чисті функції, які повернуть новий стан для програми.
Раніше ми говорили про занепокоєння щодо побічних ефектів при зміні змінних, спільних для кількох компонентів. Redux надає реальне рішення на цьому рівні, ґрунтуючись на концепції функціонального програмування, а не на об'єктно-орієнтованому. Мінімізація граничних ефектів, кожен компонент, яким би він не був, буде діяти тільки на той стан, який йому необхідний. Ми надішлемо дію для оновлення властивості стану. Потім у кожному місці, де ця властивість буде використовуватися у вигляді змінної в компоненті, вона буде зв’язана і, отже, модифікована, все в певній дії, яка лише змінить цю властивість.

Приклад редуктора

const reducer: Reducer<AppState> = (state: AppState, action: Action) => {
  switch (action.type) {
    case "IS_LOADING":
        return {
            ...state,
            isLoading: action.payload
        };
    default:
        return state;
  }
};

Місце для практики

Ось трохи складніша діаграма, яка перекладає реальний варіант використання в рамках розробки програми.

NDD: корисне навантаження - це ім'я, дане змінній будь-якого типу, яка буде надіслана дії та використана редуктором.

Давайте візьмемо випадок, коли ми створюємо програму, яка виглядає як блог, але має справу з мангою (щоб змінити статті).
У нас буде компонент, який відображатиме дані манги через її ідентифікатор (присутній в URL-адресі). Уявімо, що назва манги, її автор і кількість томів — це дані, які завантажуються під час відображення сторінки, а не її опис. Цей дуже довгий, ви повинні натиснути кнопку, щоб відобразити його, а отже, завантажити його.
Якщо натиснути цю кнопку, станеться кілька речей. Наш компонент відправить дію, яка завантажить опис манги за допомогою функції loadMangaDescription(). Ця функція буде насамперед відправити дію DESCRIPTION_IS_LOADING з корисне навантаження на true. Ми хочемо показати користувачеві, що інформація завантажується. Наприклад, якщо це значення істинно, ми можемо відображати спінер. Тому компонент отримає це значення зі стану, який був оновлений редуктором, викликаним дією DESCRIPTION_IS_LOADING.
Після того, як ми повністю синхронно оновили наш стан, щоб сказати, що опис завантажується, ми можемо рухатися далі. Компонент не тільки відправив цю дію, але також викликав службу, яка отримає опис за допомогою виклику HTTP. Хто каже, що HTTP тут говорить асинхронно, тому ми не знаємо, коли надійде відповідь. Зазвичай саме тому відображаються спінери, як у цьому прикладі.
Коли запит повернеться, служба поверне результат компоненту, який нарешті зможе запустити наступні дві дії (з’єднавшись із результатом за допомогою Promise або Subscription). Таким чином, компонент зможе знову відправити дію DESCRIPTION_IS_LOADING цього разу з корисним навантаженням на false.
Але це ще не все. Метою нашої операції з компонента є отримання опису манги. Під час завантаження можна ввімкнути спінер і видалити його після отримання даних, але все одно доведеться використовувати ці дані. Для цього ми надішлемо другу дію після першої, яка встановить для завантаження значення false, що буде LOAD_DESCRIPTION. Ця дія буде містити в корисному навантаженні дані, які ми хочемо надіслати редуктору, а потім знайти в стані. Саме ці дані потім відображатимуться в інтерфейсі користувача, наданому компоненту.
Код продукту для цього прикладу можна знайти тут Репозиторій Github щоб отримати глибше розуміння використання Redux за межами теорії та схем. Він відносно простий і відповідає запуску NgRedux, перш за все має необхідне для роботи архітектури.

Епос

Іншою концепцією Redux є концепція Epics. У попередньому випадку ми хотіли б, наприклад, не бути зобов’язаними вказувати, що після того, як виклик служби було зроблено і дія відправлена, ми також хочемо, щоб завантажувач зник, відправивши нову дію. Якби нам довелося відправити дію, яка заповнює дані в кілька місць, нам довелося б дублювати код, щоб завантажувач зник. Ось тут і з’являються епоси. Epic — це функція, яка запускається редуктором і виконає потік дій, щоб повернути інший потік дій. У нашому випадку досить було б сказати, що кожен раз дію LOAD_DESCRIPTION відправлено, ми також хочемо відправити дію DESCRIPTION_IS_LOADING з корисним навантаженням, встановленим на false. Простий та ефективний, без дублювання коду чи додаткових справ.
Більше інформації про концепцію Epics та варіанти їх використання:
- https://medium.com/kevin-salters-blog/epic-middleware-in-redux-e4385b6ff7c6
- https://redux-observable.js.org/docs/basics/Epics.html

Angular-Redux

До цього часу залишається одне питання. Ми знаємо, як слухати події на компоненті, щоб відправляти наші дії. Ми знаємо, як підключити до нього службу, якщо нам потрібні асинхронні дані, і як ефективно направляти наші дії на редуктори, щоб зміни в стані даних не стикалися. За винятком того, що після того, як ми оновили наш стан, як отримати елементи, все ще трохи незрозуміло.
Як дізнатися, коли змінна змінила значення? І як використовувати це нове значення в нашому компоненті?
Бібліотеки люблять відповідати на такі запитання Angular-Redux. Це проміжне програмне забезпечення, яке дозволить нам діяти та отримувати елементи з нашого стану статично або динамічно. Ось зразок коду для динамічного отримання опису нашої манги.

import { select } from '@angular-redux/store';
import { Observable } from 'rxjs/Observable';
import { MangaState } from '../store/state';
@Component({
    selector: 'app-manga',
})
export class MangaComponent {
    @select(['manga']) readonly manga: Observable;
    constructor() {}
}

Взятий із коду зі сховища вище та спрощений, цей код дозволяє нам мати огляд отримання наших даних із сховища.
Перше ключове слово в рядку – це @вибрати який є декоратором, наданим angular-redux і який дозволяє нам отримати наш елемент манги з магазину і зробити його доступним для спостереження. Що відповідає типу нашої змінної, беручи як вхідний інтерфейс MangaState, який відповідає типу нашого об’єкта.
Звідти буде легко відображати дані в HTML. Однак для керування Observable потрібно внести невелику зміну.

<div *ngIf="manga | async; let manga">
    Nom du manga : {{ manga.name }}
    Auteur du manga : {{ manga.author }}
    <div>LOADING DESCRIPTION</div>
    <div>{{ manga.description }}</div>
</div>

Як бачите, вам доведеться керувати Observable асинхронно. Observable - це потік, який може мати фіксовані значення в певний момент часу, але щоб відобразити ці значення в HTML, вам потрібно помістити трубу | асинхронний який автоматично отримає останнє значення. Друге зауваження, за каналом ви можете створити локальну змінну, щоб вам не доводилося робити асинхронний канал для відображення кожного значення (що було б можливо). Отже, {{ manga.name }} фактично відображатиме властивість name створеної локальної змінної манги, а не Observable.

Зворотній зв'язок

Працюючи над додатками Angular з нуля з Redux і без, я б сказав, що є хист до «думати Redux», але це відбувається досить швидко. Як і при переході від мови об’єктів до функціональної мови, ви повинні звикнути до іншого кодування. Але гра однозначно варта зусиль, а комбінація Redux + введення тексту + тестування є виграшною комбінацією для програми надзвичайно міцний, легко налагоджуваний і ремонтопридатний. Очевидно, його реалізація займає більше часу, оскільки Store має бути добре продуманий відповідно до потреб програми. Розділення даних між різними редукторами також може бути точкою, яка визначатиме ремонтопридатність залежно від повторного використання цих редукторів різними сторінками. Redux — це відкрита архітектура, в якій ви навіть можете додавати шаблони, що відповідають вашим потребам, наприклад, замінити Epics іншою системою диспетчерів, які будуть нести виключну відповідальність за диспетчерські дії, наприклад.

Висновок

Ми бачимо, що така архітектура, як Redux, може взаємодіяти з будь-яким типом програми. Вам просто потрібно повністю зрозуміти корисність шаблону для керування даними та глобальним станом вашої програми.
Вам просто потрібно подумати про додаткові можливості, щоб інтегрувати його в Angular, наприклад Angular-Redux, щоб керувати зв’язуванням між змінними компонентів і глобальним станом.
Використання Typescript набуває свого повного значення, зокрема через введення сховища та даних, які він містить, які можна знайти в редукторах, а також у діях і навіть у Observables в компонентах (дивіться код із репозиторію). Тому ми маємо на всьому коді захищеність, яку надає введення для максимально уникати помилок.
Ще один важливий момент: Redux - це не магія. Звичайно, ви повинні усвідомити, що Redux — це не що інше, як архітектура, додана до вашої програми, але вона не усуває чарівним чином усі проблеми шаблонів, які вже можуть мати ваша програма. Навіть якщо бібліотека забезпечує потужне рішення для управління даними програми, факт залишається фактом, що для її реалізації потрібен час. Ми повинні ретельно продумати структуру даних та їх нормалізацію, оскільки це матиме значний вплив на архітектуру дій, сховища та редукторів. Вам потрібно витратити час, щоб ввести та перевірити ці дані, а також функції, які їх змінюють, щоб програма була здоровою, функціональною та без сюрпризів.
Написав Гійом Барранко