click fraud detection
click fraud detection
Blog Case

Использование Redux в Typescript приложениях Angular

BLOG
CASE
437
0
5/ 5stars
5/5
Время чтения: 30 минут

В этом руководстве мы рассмотрим структурный паттерн программирования, называемый Redux и обратим внимание на следующие аспекты:

  • идея паттерна Redux;
  • построение мини-версии Redux хранилища;
  • привязка концепции Redux к фреймворку Angular.

Для многих приложений на основа Angular мы можем контролировать состояние системы при помощи весьма простого способа: забирать данные по http и рендерить их в компоненте пропуская вниз по дереву компонентов.

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

Передача данных через посредников -  с целью передачи данных (состояний) в произвольный компонент мы используем директиву input для того, чтобы “протолкнуть” данные вниз по дереву. Это значит, что мы имеем много промежуточных компонентов, передающих данные, для которых эти данные не предназначены.

Жесткая связь - так как мы передаем данные вниз по дереву при помощи input, мы создаем жесткую связь между компонентами в иерархии родитель-потомок. Зачастую, в этом нет никакой необходимости. Мало того, это делает невозможным использование дочернего компонента вне контекста его родителя без явного изменения нового контекста, где мы обязаны соблюсти эту связь и передать состояние input-ом.

Несовпадение дерева состояний с деревом DOM - Структура и иерархия состояний системы часто не соответствует структуре и иерархии элементов DOM документа. При передачи данных через дерево компонентов мы сталкиваемся с трудностями извлечения данных из его дальних веток.

Единое хранилище состояний - если мы управляем состояниями через компоненты, очень сложно получить моментальный снимок всех текущих состояний системы. Трудно определить какому компоненту принадлежат определенные данные, а какой компонент просто отслеживает их изменения.

Вынесение данных из компонентов и помещение их в сервисы, отлично решает вышеперечисленные проблемы. По крайней мере, теперь сервисы являются “владельцами” данных и мы точно знаем в каком месте они хранятся и где их нужно изменять. Но возникает вопрос: как эффективно использовать такой подход и существуют ли приемы или шаблоны наиболее оптимального использования такого “сервис-ориентированного” хранения данных.

К счастью, такие паттерны существуют и в данной статье мы рассмотрим один из них, называемый Redux.

Redux.

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

Структура данных может быть достаточно сложной темой, однако Redux предлагает настолько простое решение, что оно может быть описано менее 100 строк кода и применимо к любой, самой сложной структуре данных.

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

Для Angular инфраструктуры существует 2 библиотеки, имплементирующие идею Redux это ngrx/store и angular2-redux. Одна из которых использует принцип отслеживаемых объектов, а вторая использует Redux в качестве зависимости и внедряет ряд хэлперов.

Мы не будем использовать ни одну из них. Вместо этого, будем напрямую использовать саму концепцию Redux.

Redux. Ключевые моменты.

Ключевыми моментами идеи можно назвать следующие:

Все данные (состояния) вашего приложения находятся в одном единственном хранилище.

Приложение читает данные из этого хранилища.

Приложение никогда не изменяет данные напрямую, а вызывает определенные действия (actions), определяющие характер этих изменений .

Новое состояние системы создается комбинацией “старого состояния” и самого действия в специальной функции-переходнике, называемой reducer.

В оставшейся части статьи мы постараемся воплотить этот механизм в действие.

Что такое функция-переходник reducer?

Это функция которая принимает старое значение и действие и возвращает новое значение. Это должна быть “чистая” функция что значит:

  • Она не должна изменять данные напрямую.
  • Она не должна использовать никакие данные за пределами своих аргументов.

Другими словами “чистая функция” всегда возвращает один и тот же результат при передачи ей одних и тех же аргументов. И такая функция НИКОГДА не вызывает другие функции, которые могут изменить данные за ее пределами (не лезет в базу, не вызывает http запросов и всего того что может изменить данные снаружи).

Редьюсер всегда должен воспринимать текущее состояние в режиме “только-чтения”. Редьюсер сам не изменяет состояние, он только возвращает новое состояние. Часто это новое состояние представляет собой копию старого, но об этом позднее.

Давайте определим наш первый редьюсер. Как мы помним оперировать будем тремя сущностями:

  1. Действие. Что делаем.
  2. Состояние. Где делаем.
  3. Редьюсер. Принимает действие и состояние и возвращает новое состояние.
     

Определим интерфейс Действия (Action).

interface Action {
  type: string;
  payload?: any;
}

Тип будет строкой, которая определяет тип действия например ADD_USER, DELETE_USER и т.д.

Payload - необязательный объект любого типа.

Интерфейс редьюсера.

interface Reducer {
  (state: T, action: Action): T;
}

Тут мы используем генерик T (обобщение) в котором определяем тип данных состояния state. Обратите внимание, в этом интерфейсе мы говорим что наш редьюсер имеет метод, который принимает два аргумента state (типа T) и action (типа Action) и возвращает значение state типа T.

Создание первого редьюсера.

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

let reducer: Reducer = (state: number, action: Action) => {
  return state;
};

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

Вызвать наш редьюсер можно строкой

console.log( reducer(0, null) );

В данной строке мы передаем null в качестве Action и ничего полезного не делаем за исключением возвращения начального состояния 0.

Для того чтобы изменить state, нам необходимо проанализировать объект Action, предварительно его создав.

let decrementAction: Action = { type: 'DECREMENT' }
let incrementAction: Action = { type: 'INCREMENT' }

let reducer: Reducer = (state: number, action: Action) => {
  if (action.type === 'INCREMENT') {
    return state + 1;
  }
  if (action.type === 'DECREMENT') {
    return state - 1;
  }
  return state;
};

Теперь можно воспользоваться нашим редьюсером.

let incrementAction: Action = { type: 'INCREMENT' };

console.log( reducer(0, incrementAction )); // -> 1
console.log( reducer(1, incrementAction )); // -> 2

let decrementAction: Action = { type: 'DECREMENT' };

console.log( reducer(100, decrementAction )); // -> 99

Мы видим, что возвращаемой значение соответствует типу действия, которое мы загоняем внутрь редьюсера.

Более удобным способом будет использование оператора switch вместо if.

let reducer: Reducer = (state: number, action: Action) => {
  switch (action.type) {
  case 'INCREMENT':
    return state + 1;
  case 'DECREMENT':
    return state - 1;
  default:
    return state; //   }
};

Обратите внимание на действие по умолчанию где мы просто возвращаем оригинальный state. Это исключит неопределенность в случае если мы передадим неизвестный объект Action ошибки не возникнет и мы получим оригинальный state.

Использование параметра payload позволит усложнить процесс и добавить возможность указать значение, на которое изменять state.

let plusSevenAction = { type: 'PLUS', payload: 7 };
...
  case 'PLUS':
    return state + action.payload;
...

Сохранение состояния.

В примерах выше мы не сохраняли новое состояние, в реальных приложениях мы должны это делать и где то его хранить т.к. Сам редьюсер ничего не изменяет за его пределами. В Redux мы храним наши состояния в хранилище store.

Хранилище отвечает за запуск редьюсеров и хранение новых значений состояний.

Взглянем на простейший пример хранилища.

class Store {
  private _state: T;

  constructor(
    private reducer: Reducer,
    initialState: T
  ) {
    this._state = initialState;
  }

  getState(): T {
    return this._state;
  }

  dispatch(action: Action): void {
    this._state = this.reducer(this._state, action);
  }
}

Мы также используем дженерики для обозначения типа приватного объекта-хранилища _state и для редьюсера который оперирует объектом _state так как каждое хранилище store привязано к конкретному редьюсеру. Мы храним редьюсер в приватной переменной reducer.

В Redux мы имеем одно хранилище и один редьюсер верхнего уровня.

Давайте разберем каждый метод нашего хранилища.

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

getState() - просто возвращает текущее состояние.

Dispatch - принимает action, передает его в редьюсер и обновляет значение _state возвращаемым значением.

Заметте, dispatch ничего не возвращает. Он только обновляет состояние хранилища. Это важный принцип Redux - диспетчер действует на манер “выполнил-и-забыл”. Когда мы вызываем диспетчер мы уведомляем его о том что произошло. Если мы хотим знать состояние хранилища, мы должны вызвать другой метод getState().

Пример использования хранилища.

let store = new Store(reducer, 0);
console.log(store.getState()); // -> 0

store.dispatch({ type: 'INCREMENT' });
console.log(store.getState()); // -> 1

store.dispatch({ type: 'INCREMENT' });
console.log(store.getState()); // -> 2

store.dispatch({ type: 'DECREMENT' });
console.log(store.getState()); // -> 1

Уведомления при помощью подписки на событие.

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

Нам необходимо сделать следующее:

  1. Зарегистрировать функцию-слушателя, используя метод subscribe.
  2. Когда вызывается метод dispatch, мы будем проходить по всем зарегистрированным функциям-слушателям и последовательно их применять, тем самым уведомляя части приложений об изменениях в нашем хранилище.
     

Регистрация слушателя.

Наша функция-слушатель не будет принимать никаких аргументов. Давайте определим интерфейс, который описывает такую функцию.

interface ListenerCallback {
  (): void;
}

После подписки слушателя нам также необходимо иметь функцию для отписки.

interface UnsubscribeCallback {
  (): void;
}

Данный код не делает ничего полезного но становится более читабельным.

Хранилище должно содержать список таких функций.

 class Store {
  private _state: T;
  private _listeners: ListenerCallback[] = [];

Теперь мы должны иметь возможность добавлять наших слушателей в этот список методом subscribe.

 class Store {
  private _state: T;
  private _listeners: ListenerCallback[] = [];

  subscribe(listener: ListenerCallback): UnsubscribeCallback {
    this._listeners.push(listener);
    return () => { // возвращаем функцию “отписки”
      this._listeners = this._listeners.filter(l => l !== listener);
    };
  }

Метод subscribe принимает в качестве аргумента функцию-подписчик и возвращает функцию для отписки от события изменения. Добавляется функция в список методом push.

Уведомляем подписчиков.

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

  dispatch(action: Action): void {
    this._state = this.reducer(this._state, action);
    this._listeners.forEach((listener: ListenerCallback) => listener());
  }

Полный код хранилища Store.

class Store {
  private _state: T;
  private _listeners: ListenerCallback[] = [];

  constructor(
    private reducer: Reducer,
    initialState: T
  ) {
    this._state = initialState;
  }

  getState(): T {
    return this._state;
  }

  dispatch(action: Action): void {
    this._state = this.reducer(this._state, action);
    this._listeners.forEach((listener: ListenerCallback) => listener());
  }

  subscribe(listener: ListenerCallback): UnsubscribeCallback {
    this._listeners.push(listener);
    return () => { // returns an "unsubscribe" function
      this._listeners = this._listeners.filter(l => l !== listener);
    };
  }
}

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

let store = new Store(reducer, 0);
console.log(store.getState()); // -> 0
 
// подписка
let unsubscribe = store.subscribe(() => {
  console.log('подписан: ', store.getState());
});
 
store.dispatch({ type: 'INCREMENT' }); // -> подписан: 1
store.dispatch({ type: 'INCREMENT' }); // -> подписан: 2
 
unsubscribe();
store.dispatch({ type: 'DECREMENT' }); // (ничего не происходит)

// изменения произойдут даже если никто на них не подписан
console.log(store.getState()); // -> 1

Этот пример может быть переписан с использованием библиотеки RxJS, которая имплементирует паттерн Observers с помощью специальных объектов типа Observable, вместо того, чтобы писать свой собственный.

У ВАС ОСТАЛИСЬ ВОПРОСЫ?

Оставьте ваши контактные данные. Наш менеджер свяжется и проконсультирует вас.

ПОЛУЧИТЬ КОНСУЛЬТАЦИЮ

Наш менеджер свяжется с Вами в ближайшее время

5/5
Проголосовало людей: 1
СОДЕРЖАНИЕ
СТАТЬИ
Redux.
Redux. Ключевые моменты.
Что такое функция-переходник reducer?
Сохранение состояния.
Уведомления при помощью подписки на событие.
Регистрация слушателя.
Уведомляем подписчиков.
Что такое SPA-приложения
Если вы хотите лучше разобраться в этой теме, то данная статья будет вам полезна. В…
Алексей Варламов
Алексей Варламов
Лучшие PHP-фреймворки, которые упрощают разработку в 2019 году
Фреймворки PHP – это программные платформы, которые значительно облегчают и ускоряют разработку сайтов, web- и…
Алексей Варламов
Алексей Варламов
Что такое модернизация сайта и когда она необходима
В этой статье мы расскажем о том, какие задачи позволяет решить данный вид работ, какие…
Алексей Варламов
Алексей Варламов
Разработка и запуск персонального сайта
В этой статье мы расскажем о том, что из себя представляет персональный сайт и каким…
Алексей Варламов
Алексей Варламов
ПОЛУЧАТЬ ИНТЕРЕСНЫЕ СТАТЬИ
Уже подписались 253 человек
Автор
437
0
Дмитрий Жариков
Дмитрий
Жариков
most
Popular
Возможно
В этой статье мы расскажем о прототипировании: что это, зачем нужен прототип и почему специалисты…
Алексей Варламов
Алексей Варламов
Нативное или гибридное приложение? В чем разница? Преимущества и недостатки каждого вида приложений.
Галина Назарова
Галина Назарова
В этой статье мы собрали несколько ключевых причин, из-за которых сайт может терять посетителей, а…
Алексей Варламов
Алексей Варламов
Давайте начнем
беседу!
КОММЕНТАРИИ0
ОСТАВИТЬ КОММЕНТАРИЙ К СТАТЬЕ
ПОДПИСЫВАЙТЕСЬ НА РАССЫЛКУ АЙТЫЖБЛОГ
ХОТИТЕ ПОЛУЧАТЬ 
ИНТЕРЕСНЫЕ СТАТЬИ?
Уже подписались 253 человек
313
ПОПИСЧИКОВ
ЧИТАТЬ
4295
ПОПИСЧИКОВ
СЛЕДИТЬ
9307
ПОПИСЧИКОВ
СЛЕДИТЬ