Angular/NGRX — ясное и чёткое введение

Дополненный и исправленный перевод

Angular/NGRX — ясное и чёткое введение

Оригинал статьи репозиторий GitHub Документация ngrx Справочник RxJS

Цель этой статьи — дать ясное и понятное введение в ngrx. Я объясню, что вам нужно знать об ngrx, а затем проиллюстрирую это с помощью нескольких простых и понятных примеров кода.

В конце будет ссылка на репозиторий GitHub, чтобы вы могли поиграться с кодом.

Вот что мы рассмотрим:

ngrx это группа библиотек, «вдохновленная» библиотекой Redux, которая, в свою очередь, «вдохновлена» шаблоном Flux. Это означает, что шаблон Redux является упрощенной версией шаблона Flux, а NGRX является версией шаблона redux с использованием Angular и RxJS.

Что я подразумеваю под «angular/rxjs» версией redux
«Angular» — потому что ngrx — это библиотека для использования в Angular приложениях.
«rxjs» — потому что реализация ngrx работает с использованием потоков observables, и различных операторов, предоставляемых «rxjs».

Главная цель этого шаблона — соорудить контейнер для состояний, работающий по строгим правилам. Они основаны на трёх принципах.

Давайте пройдемся по трем принципам библиотеки Redux, и укажем на наиболее важные преимущества, которые они предоставляют.

Единый источник правды

Для архитектуры redux и ngrx это означает, что состояние всего вашего приложения хранится в древовидном объекте, — в одном хранилище.

Что значит в одном хранилище? Позже мы поговорим о хранилище, но в общем случае оно несёт ответственность за сохранение состояния и применение к нему изменений, когда ему говорят об этом (например, когда отправляется действие - мы рассмотрим это позже).

Преимущества наличия единственного источника правды более чем достаточны, но поскольку он будет влиять на любое приложение angular, для меня более интересно следующее:

  • Когда вы создаете приложение angular, вы обычно разделяете состояние, и обрабатываете их в разных сервисах. По мере того как ваше приложение растёт, отслеживание изменения состояний становится беспорядочным, их трудно отлаживать и поддерживать. Наличие единственного источника правды решает эту проблему, поскольку состояние обрабатывается только в одном объекте и в одном месте, поэтому отладка или добавление изменений становится намного проще.

Состояние только для чтения

Вы никогда не изменяете состояние (state) напрямую, вместо этого вы отправляете действия (dispatch actions). Эти действия описывают, что происходит. Например: получение, добавление, удаление, обновление состояния.

Отправка действия? … Мы поговорим об этом позже, но в основном они являются идентификаторами операции в вашем приложении, и могут быть вызваны/отправлены (triggered/dispatched), чтобы попросить приложение выполнить операцию, которую описывает действие.

Вы получаете много преимуществ, избегая обновления состояния из разных мест, и имея централизованное место для внесения изменений, которое реагирует на конкретные действия. Вот самые важные из них:

  • Вы знаете, что любое изменение в состоянии произойдет только в одном месте. Это сильно облегчает отладку и тестирование.
  • Вы знаете, что при отправке определенного действия операция в состоянии всегда одинакова. Опять же, это облегчает отладку и тестирование.

Изменения делаются чистыми функциями

Операция, инициируемая отправкой действия, будет чистой функцией, называемой в архитектуре redux — reducer (редукторами).

Эти редукторы (просто чистые функции) получают действие (action) и состояние (state), в зависимости от отправленного действия (обычно отфильтрованного оператором switch), они выполняют операцию и возвращают новый объект состояния.

Состояние в redux приложении является неизменным (immutable)! Поэтому, когда редуктор (reducer) изменяет что-либо в состоянии, он возвращает новый объект состояния.

Преимущества использования чистых функций хорошо известны, например то что они сразу готовы к тестированию. Если вы передадите те же аргументы, то получите тот же результат.

Этот подход также позволяет нам перемещаться между различными экземплярами нашего состояния с помощью инструментов разработки Redux/ngrx и видеть, что изменилось между экземплярами, кто их изменил, и многое другое. Поэтому использование чистых функций и возвращение новых экземпляров состояния также значительно облегчает отладку.

Но главное преимущество, на мой взгляд, заключается в том, что, привязав все входные данные наших компонентов к свойствам состояния (state), мы можем изменить стратегию обнаружения изменений (angular/zoneJS change detection) на onPush, и это улучшит производительность приложения.

Отлично … так каковы же преимущества использования NGRX

Мы уже упоминали большинство из них, когда говорили о принципах шаблона Redux. Но давайте отметим наиболее важные преимущества использования шаблона Redux в приложении (на взгляд автора):

  • Поскольку у нас есть один источник правды, и вы не можете напрямую изменить состояние, приложения будут работать более согласованно.
  • Использование шаблона Redux дает нам много интересных функций (dev tools), облегчающих отладку.
  • Тестирование приложений становится проще, поскольку мы вводим чистые функции для обработки изменений состояния, а также потому, что и ngrx, и rxjs, имеют множество замечательных возможностей для тестирования.
  • Как только вы почувствуете себя комфортно при использовании ngrx, понимание потока данных в ваших приложениях станет невероятно простым и предсказуемым.

… и недостатки

  • У NGRX, конечно, есть кривая обучения. Не большая, но и не такая маленькая, и я думаю, что это требует некоторого опыта или глубокого понимания некоторых программных шаблонов. Это не является проблемой для любого разработчик среднего уровня, но младший может поначалу немного запутаться.
  • Для меня это ощущается немного многословно (прим пер.: речь о проблеме множества заготовок кода — heavy boilerplate). Поэтому каждый раз, когда вы добавляете какое-либо свойство в состояние (state property), вам нужно добавлять действия (actions) и диспетчеры (dispatchers). Вам может потребоваться обновить или добавить селекторы (selectors), эффекты (effects), если таковые имеются, и обновить хранилище (store). Кроме того, вы будете собирать конвейер (pipe) операторов rxjs и наблюдаемых потоков (observables) везде где это потребуется.
  • NGRX не является частью библиотек Angular core, и не поддерживается Google. По крайней мере, не напрямую, потому что среди контрибьюторов ngrx есть разработчики из команды Angular. Это ещё один пункт для обдумывания — вы добавляете в зависимости тяжёлую библиотеку.

Когда использовать NGRX

Итак, по общему мнению, ngrx следует использовать в средних/крупных проектах, где управление состоянием становится трудным в обслуживании. Те кто фанатеют по шаблонам (pattern) скажут что-то вроде «если у вас есть состояние, у вас есть NGRX».

Я согласен, что его следует использовать в средних или крупных проектах, когда у вас есть значительное количество состояний, и множество компонентов, использующих эти состояния. Но вы должны учитывать, что Angular сам по себе предоставляет множество решений для управления состоянием. Если у вас есть сильная команда разработчиков, то, возможно, вам не нужно беспокоиться о ngrx.

При этом я считаю, что сильная команда разработчиков Angular может решить использовать ngrx в проекте потому что они знают силу шаблона Redux и операторов rxjs. И они чувствуют себя комфортно, работая и с тем, и с другим …

Если вы ожидали простого ответа, чтобы решить, когда использовать ngrx, вы не получите его, и не доверяйте никому, кто дает вам этот ответ за пределами вашей организации или команды. Решение зависит от изучения плюсов и минусов, понимания возможностей вашей команды, и учета их мнения.

Действия (actions), редукторы (reducer), селекторы (select), хранилище (store) и побочные эффекты (effects) NGRX

Это основные строительные единицы жизненного цикла ngrx. Каждый из них берет на себя часть процесса от запуска операции до изменения нашего состояния и извлечения данных.

На этой картинке мы видим жизненный цикл ngrx. Давайте разберём его …

1. В наиболее распространенном сценарии все начинается в представлении компонента (component view). Взаимодействие с пользователем может привести к тому, что компонент отправит действие (dispatch action).

Действия (actions)…
В объекте хранилища (store object) у вас есть функция для отправки/запуска (dispatch/trigger) действий. Действия — это классы, которые реализуют интерфейс действий NGRX/Actions. Эти классы действий имеют два свойства (давайте возьмем в качестве примера класс действия с именем GetUserName):
тип (type): это обычная строка только для чтения, описывающая, что означает действие. Например: ‘[User] Get User Name’
Полезная нагрузка (payload): тип этого свойства зависит от того, какой тип данных это действие необходимо отправить редуктору (reducer). В случае с предыдущим примером это будет строка, содержащая имя пользователя. Не все действия требуют полезной нагрузки.

2.1. Если действие не вызывает эффект (trigger effect), то редуктор отфильтрует действие (обычно с помощью оператора switch), и вернёт новое состояние, которое будет результатом слияния старого состояния со значением, которое изменилось после вызова действия.

Редукторы (reducers)…
Редукторы — это чистые функции, принимающие два аргумента: предыдущее состояние (state) и действие (action). Когда отправляется действие, ngrx проходит через все редукторы, передавая в качестве аргументов предыдущее состояние и действие, в порядке, в котором редукторы были созданы, пока не найдет обработчик для этого действия.

2.2. Если действие вызвало эффект, то это говорит о необходимости обработки побочных эффектов перед вызовом редуктора. Это может быть что-то вроде вызова службы HTTP для получения данных.

Эффекты (effects)…
Эффекты в экосистеме библиотек ngrx позволяют нам иметь дело с побочными эффектами (прим. пер.: обычно это функция изменения состояния данных), вызванными отправкой действия вне компонентов Angular или хранилища ngrx.
Эффекты прослушивают отправленные действия, и, также как и редукторы, проверяют, имеются ли у них обработчик для них. Затем выполняется побочный эффект. Обычно это получение или отправка данных посредством API.
Потом будет выполнено следующее действие, обычно относящееся к результирующему состоянию побочного эффекта (успех, ошибка отправки данных, и т. д.). Затем действие обрабатывает редуктор. Мы уже упоминали это в описании жизненного цикла ngrx.

2.2.1. После того, как эффект отработал (побочные эффекты закончились), он запускает новое действие «состояние-результат» (побочные эффекты могут быть успешными или неудачными), и мы возвращаемся к пункту 2.1.

3. Теперь у хранилища есть новое состояние. Состояние может быть большим деревом — объектом, поэтому ngrx вводит селекторы, чтобы иметь возможность использовать только необходимые фрагменты объекта.

Селекторы (select) …
Как мы упоминали ранее, дерево состояний может стать довольно большим объектом, и не имеет смысла размещать весь этот объект там, где нужна только его часть.
Хранилище NGRX предоставляет нам функцию «селектор» для получения фагментов нашего хранилища. А если нам нужно применить некоторую логику к этому фрагменту перед использованием данных в компонентах?
Здесь в игру вступают селекторы. Они позволяют нам обрабатывать данные фрагмента состояния вне компонента. Функция «select» хранилища принимает в качестве аргумента чистую функцию, она и является нашим селектором.
Хранилище (store)…
Хранилище — это объект (экземпляр класса ngrx/Store), который объединяет вещи, о которых мы упоминали ранее (действия, редукторы, селекторы). Например, когда через его функции отправляется действие,то хранилище находит и выполняет соответствующий редуктор.
Оно также хранит состояние приложения.

Пример использования NGRX

Итак, мы закончили с теорией, представив жизненный цикл ngrx и его участников, а теперь пришло время посмотреть на это в действии. Из этого легко сделать ещё одну статью. Но, на мой взгляд, нет смысла объяснять всё то, что мы объяснили без примеров реализации. Так мы можем посмотреть всё в действии, скачать и поиграться с кодом.

В нашем примере будет список пользователей, страница сведений о пользователе, и некоторая начальная информация о конфигурации, которую вы должны получить при запуске приложения. Мы собираемся реализовать некоторые важные этапы жизненного цикла ngrx.

Вот что это будет:

  • Установить библиотеки ngrx
  • Создать структуру папок для хранилища Store
  • Создать состояние State и начальные значения
  • Создать действия Actions
  • Создать редукторы Reducers
  • Создать эффекты Effects
  • Создать селекторы Selectors
  • Собрать и настроить всё вместе
  • Использовать хранилище в некоторых компонентах

Итак, приступим …

Установка библиотеки

Мы собираемся использовать Angular Cli для создания проекта, а затем добавить библиотеки ngrx.

Давайте создадим проект:

ng new angular-ngrx —style=scss

Давайте добавим библиотеки ngrx, которые мы будем использовать:

npm install @ngrx/core @ngrx/store @ngrx/effects @ngrx/store-devtools @ngrx/router-store --save

Мы устанавливаем почти все библиотеки экосистемы ngrx. Названия большинства из них совершенно ясно представляют их назначение. Например, ядро core, хранилище store, эффекты effects. Но есть пара, которая может удивить вас своим предназначением.

  • store-devtools — мощная утилита для отладки.
  • router-store — сохраняет состояние маршрутизатора Angular в хранилище.

Структура папок хранилища

Давайте начнем с обсуждения файловой структуры хранилища. Эта файловая структура и вся конфигурация хранилища должны быть в core модуле вашего приложения. Но в нашем примере у нас его нет, поэтому хранилище будет расположено в app модуле (действия в значительной степени совпадают, если расположить его в core модуле).

Структура папок хранилища

Структура папок представляет фактическую структуру объекта хранилища. У вас будет главная папка с названием «store» и пять вложенных папок, которые представляют каждого из ключевых игроков хранилища: «Actions», «Effects», «Redurs», «Selectors» и «State».

Создание состояния и начальных значений

Как мы уже упоминали ранее, в нашем приложении будет две основных структуры: пользователи users и конфигурация config . Для каждого из них нам нужно создать объект состояния и начальное значение, а также сделать то же самое для состояния приложения app.

Мы создали два интерфейса для определения объекта пользователя и конфигурации. У нас также есть ещё один для результата HTTP запроса получения объекта пользователя — это просто массив IUser.

интерфейс IUser
интерфейс IConfig

Начнём с состояния пользователя: store/state/user.state.ts

Файл описания состояния пользователя

То, что мы сделали здесь очевидно:

  • Создаем и экспортируем интерфейс со структурой состояния пользователя.
  • Делаем то же самое с начальным значением состояния пользователя, которое реализует implement созданный интерфейс.

Для состояния конфигурации мы делаем то же самое store/states/config.state.ts:

Файл описания состояния конфигурации

Наконец, нам необходимо сгенерировать состояние приложения store/state/app.state.ts

Файл описания состояния приложения
  1. Состояние приложения содержит состояние пользователя и конфигурации, а также состояние маршрутизатора.
  2. Ещё у него есть начальное значение состояния приложения.
  3. Наконец, оно экспортирует функцию получения начального состояния (мы используем её позже).

Создание действия

Обязательно прочитайте определение действия, которое мы обсуждали в этой статье.

Нам нужно создать действия для пользователей и настройки. Начнем с действий пользователя store/actions/user.actions.ts:

Файл с описанием действий пользователя

Так что это хороший пример действий, которые создаются для функции или сущности entity в нашем приложении. Давайте немного пройдемся по коду:

  1. Мы экспортируем объект перечисляемого типа enum, содержащий определение для типов действий. Таким образом, мы избегаем использования произвольного текста, и дублирования кода для типов действий. Это легко порождает ошибки.
  2. Мы создаем и экспортируем класс для каждого из ваших действий. Все они должны реализовывать интерфейс ngrx/Action. Наконец, мы выбираем для типа действия одно из значений перечисляемого объекта enum. Если вам нужна полезная нагрузка действия payload, вы добавляете её в конструктор класса.
  3. Наконец, мы экспортируем тип UserActions, содержащий наши классы действий. Это обеспечит нам проверку типов typescript, которую мы можем использовать, например, в наших редукторах reducer.

Ну вот всё … создавать действия просто. Давайте посмотрим, как выглядят действия конфигурации store/actions/config.actions.ts:

Файл с описанием действий конфигурации

Здесь нет ничего нового, и вы, скорее всего, теперь можете легко понять содержимое файла.

Отлично, мы уже определили состояние и действия … давайте создадим редукторы!

Создание редукторов (reducers)

Обязательно прочтите определение редукторов, которое мы обсуждали в этой статье.

У нас будут редукторы, реагирующие на некоторые действия, потому что другие будут обрабатываться эффектами, которые мы собираемся реализовать позже.

Нам понадобится редуктор для пользователей и другой для конфигурации, но нам также понадобится генерировать редукторы приложений, давайте начнем с рассмотрения редукторов пользователей store/reducers/user.reducers.ts:

Файл с описанием редукторов пользователя

Давайте обсудим реализацию:

  1. В объявлении метода редуктора userReducers() указываем параметры состояния и действия пользователя. Метод возвращает состояние, реализующее интерфейс IUserState.
  2. Используя оператор switch, выбираем обработчики для каждого возможного типа действия.
  3. В обработчике типа действия возвращаем новый объект, который является результатом слияния старого состояния и нового значения.
  4. У каждого редуктора есть результат по-умолчанию, который просто возвращает состояние без каких-либо изменений.

И это всё. В редукторе больше ничего нет. Давайте посмотрим на редукторы конфигурации state/reducers/config.reducers.ts:

Файл с описанием редукторов конфигурации

Глядя на этот код, вы, вероятно, легко его понимаете, т. к. мы уже это всё обсуждали.

Наконец, давайте посмотрим на редукторы приложения store/reducers/app.reducers.ts:

Файл с описанием редукторов приложения

Здесь мы добавляем все редукторы reducers в связанный список map, обрабатывающий действия приложения. Мы используем тип ActionReducerMap для добавления проверки типов действий. Позже мы предоставим provide редукторы этого приложения в модуле angular module хранилища.

Отлично!… Теперь у нас есть наше состояние, наши действия, наши редукторы, но нам всё ещё нужны эффекты и селекторы. Давайте добавим наши эффекты …

Создание эффектов

Обязательно прочтите определение «Эффектов», которое мы уже обсуждали в этой статье.

Вы, наверное, уже заметили, что в редукторах мы не обрабатываем все действия, которые мы создали. Это потому, что мы собираемся обработать пропущенные действия в эффектах, потому что эти действия имеют побочные эффекты.

Давайте начнем с пользовательских эффектов store/effects/user.effects.ts:

Файл с описанием эффектов пользователя

В этом файле у нас много чего происходит. Давайте попробуем объяснить это:

  1. Мы объявляем наши пользовательские эффекты с помощью декоратора внедряемых зависимостей injectable decorator.
  2. Мы объявляем наши эффекты, используя декоратор эффектов, предоставленный библиотекой ngrx/Effects.
  3. Используя действия, предоставляемые ngrx/Effects, мы собираемся собрать конвейер pipe наших операторов rxjs для этого эффекта.
  4. Следующим шагом является определение типа действия в эффекте с помощью оператора ofType.
  5. Следующие части представляют собой операторы rxjs, которые мы используем для получения того, что нам нужно (Документация ngrx Справочник RxJS).
  6. Наконец, в последнем операторе Effect собирается отправить другое действие
  7. В конструкторе мы внедряем сервисы, которые собираемся использовать, действия для ngrx/effects и, в данном случае, хранилище (учтите, что это демонстрационная версия, и мы получаем выбранного пользователя из списка пользователей в нашем локальном хранилище)

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

Давайте посмотрим на эффекты конфигурации store/effects/config.effects.ts:

Файл с описанием эффекта конфигурации

И снова, вы, вероятно, чувствуете себя комфортно читая этот код.

Теперь пришло время поговорить о селекторах …

Создание селекторов (selectors)

Обязательно прочитайте определение Селекторов, которое мы обсуждали в этой статье.

Нет смысла повторять получение фрагментов нашего состояния повсеместно, поэтому давайте создадим селекторы, которые можно использовать повторно.

Как всегда, давайте сначала посмотрим на пользовательские селекторы store/selectors/user.selector.ts:

Файл с описанием селекторов пользователя

Этот код просто понять, потому что мы не делаем никаких преобразований данных в наших селекторах. Вместо этого мы возвращаем фрагмент хранилища, на который ссылается селектор, используя функцию createSelector из ngrx/store.

Первый параметр — это фрагмент хранилища, которое будет использоваться для получения данных (это может быть массив с несколькими объектами состояния), второй параметр — это анонимная функция, которая будет решать, какой селектор возвращать.

Вот так создаются селекторы. Давайте посмотрим на хранилище настроек config/selectors/config.selectors.ts:

Файл с описанием состояния конфигурации

Как и раньше, вы чувствуете себя комфортно читая этот код.

Мы уже создали всё необходимое для хранилища, но нам нужно собрать всё это вместе.

Настроить всё вместе

Отлично, мы создали всё, что нужно нашему хранилищу, но нам пока не хватает одной вещи — собрать всё воедино. Я собираюсь сделать это в модуле app, но вы можете применить то же самое в модуле core вашего приложения.

Давайте посмотрим модуль приложения:

Файл с описанием модуля приложения

Давайте перечислим то, что необходимо для настройки нашего хранилища:

  1. Мы импортируем наши редукторы, и передаём их в метод forRoot модуля хранилища.
  2. Мы импортируем наши эффекты, и передаём их внутри массива в метод forRoot модуля эффектов.
  3. Мы передаём настройки для модуля состояния маршрутизатора StoreRouterConnectingModule.
  4. И мы добавляем инструменты разработчика хранилища StoreDevtoolsModule.instrument(), если запущена среда разработки.

Первые два шага необходимы, в то время как шаги 3 и 4 я настоятельно рекомендую, но они не являются обязательными.

Теперь мы наконец закончили … и можем использовать хранилище в наших компонентах!

Использование хранилища в некоторых компонентах

Может быть, сейчас вы думаете:

Великий Санти, я потратил 20 минут, читая твою статью, я знаю, что мне нужно знать о ngrx: что это такое, когда его использовать, как его настроить … и это здорово, но не могли бы вы показать мне сейчас, как я могу это использовать!

Да, я могу! Пожалуйста, не теряйте интерес, мы приближаемся к концу! Давайте посмотрим, как использовать наше хранилище …

Во-первых, давайте получим конфигурацию при запуске приложения:

Файл с описание компонента приложения
  1. Мы добавляем хранилище в наш app.component.
  2. Мы передаём в config$ значение из селектора selectConfig, потому что мы хотим показать часть этой информации в HTML.
  3. В ngOnInit мы отправляем действие, чтобы получить и передать конфигурацию в хранилище.

Вот и всё … Мы уже написали код эффекта, который будет обрабатывать это действие, и редуктор, который будет обрабатывать успех этого эффекта. Как только хранилище приобретёт новое состояние, селектор изменит значение нашего свойства. Удивительно!

Вот как мы связываем данные конифгурации в HTML:

Файл с описанием HTML разметки компонента навигации приложения

Как только у config$ изменится значение, мы увидим его в HTML.

Теперь давайте посмотрим список пользователей container/users/users.component.ts:

Я использую концепции компонентов контейнеров и презентационных компонентов (прим. пер.: умные и тупые компоненты). Если вы не знакомы с этим подходом, посмотрите здесь.
Файл с описанием компонента пользователя
  1. Мы собираемся получить список пользователей также как и конфигурацию. Сначала мы внедряем хранилище в компонент пользователя.
  2. В ngOnInit мы отправляем действие, чтобы получить пользователей.
  3. Мы создаем свойство users$, и присваиваем ему список пользователей, используя селектор selectUserList.

HTML выглядит так:

Файл с описанием HTML разметки компонента пользователя

Мы отображаем список пользователей в презентационном(тупом) компоненте, и привязываем выбранного пользователя к функции navigateToUser(), которую мы видели в контейнерном(умном) компоненте пользователя ранее.

Отлично… а как мы показываем выбранного пользователя?…

Давайте посмотрим на компонент пользовательского контейнера:

Файл с описанием контейнерного компонента пользователя

Этот компонент получает параметр id из ActivatedRoute (текущего url), а с остальным вы, вероятно, уже знакомы. Отправление id в качестве параметра, выбор выбранного пользователя…

Если вы хотите увидеть весь код, просто зайдите в репозиторий GitHub.

Для отладки вы можете использовать инструменты разработчика, которые довольно просты в использовании… но данная статья и так уже достаточно подробная, и, я надеюсь, что вы без труда разберетесь с этими инструментами.

Заключение

В этой статье я попытался предоставить ясное и чёткое введение в ngrx, описав всё, что вам нужно знать, чтобы войти в этот мир.

Мы начали объяснять понятия, разбираться, как это работает, зачем оно используется, и, наконец, мы рассмотрели полный базовый пример.

Вот репозиторий GitHub…

Я рекомендую вам скачать его и немного поиграться с кодом.

Я действительно надеюсь, что эта статья поможет вам понять принципы работы ngrx.

Немного благодарностей…

Спасибо @leosvel.perez.espinosa за то, что он уделил некоторое время обсуждению со мной некоторых функций ngrx и @fevialmeida за этот невероятный баннер!

Примечание переводчика: примеры кода дополнены комментариями, исправлены незначительные ошибки, текст незначительно сокращён для более литературно красивого перевода.