Типичное веб-приложение обычно состоит из нескольких компонентов пользовательского интерфейса, которые совместно используют данные. Часто на несколько компонентов возлагается ответственность за отображение разных свойств одного и того же объекта. Этот объект представляет состояние, которое может измениться в любое время. Сохранение согласованного состояния между несколькими компонентами может быть кошмаром, особенно если для обновления одного и того же объекта используется несколько каналов.
Взять, к примеру, сайт с корзиной покупок. Вверху у нас есть компонент пользовательского интерфейса, показывающий количество товаров в корзине. У нас также может быть другой компонент пользовательского интерфейса, который отображает общую стоимость товаров в корзине. Если пользователь нажимает кнопку « Добавить в корзину» , оба этих компонента должны немедленно обновить правильные цифры. Если пользователь решит удалить товар из корзины, изменить количество, добавить план защиты, использовать купон или изменить место доставки, тогда соответствующие компоненты пользовательского интерфейса должны обновиться для отображения правильной информации. Как видите, простая корзина для покупок может быстро стать синхронизированной по мере расширения области ее функций.
В этом руководстве я познакомлю вас с платформой, известной как Redux , которая поможет вам создавать сложные проекты таким образом, чтобы их было легко масштабировать и поддерживать. Чтобы упростить процесс обучения, мы будем использовать упрощенный проект корзины для покупок, чтобы узнать, как работает Redux. Вы должны быть по крайней мере знакомы с библиотекой React , поскольку позже вам нужно будет интегрировать ее с Redux.
Предпосылки
Прежде чем начать, убедитесь, что вы знакомы со следующими темами:
Также убедитесь, что на вашем компьютере установлены следующие настройки:
- среда NodeJS
- настройка пряжи (рекомендуется)
Вы можете получить доступ ко всему коду, используемому в этом руководстве, на GitHub .
Что такое Redux
Redux — это популярный JavaScript-фреймворк, который обеспечивает предсказуемый контейнер состояний для приложений. Redux основан на упрощенной версии Flux, фреймворка, разработанного Facebook. В отличие от стандартных сред MVC, где данные могут передаваться между компонентами пользовательского интерфейса и хранилищем в обоих направлениях, Redux строго разрешает передачу данных только в одном направлении. Смотрите рисунок ниже:
Рисунок 1: Блок-схема Redux
В Redux все данные — т.е. состояние — хранятся в контейнере, известном как хранилище . В приложении может быть только один из них. Хранилище — это, по сути, дерево состояний, в котором хранятся состояния для всех объектов. Любой компонент пользовательского интерфейса может получить доступ к состоянию конкретного объекта непосредственно из хранилища. Чтобы изменить состояние локального или удаленного компонента, необходимо отправить действие. Отправка в этом контексте означает отправку действенной информации в магазин. Когда хранилище получает action
, оно делегирует его соответствующему редуктору . reducer
— это просто чистая функция, которая просматривает предыдущее состояние, выполняет действие и возвращает новое состояние. Чтобы увидеть все это в действии, нам нужно начать кодирование.
Сначала поймите непреложность
Прежде чем мы начнем, мне нужно, чтобы вы сначала поняли, что означает неизменность в JavaScript. Согласно Оксфордскому словарю английского языка, неизменность означает неизменность . В программировании мы пишем код, который постоянно меняет значения переменных. Это называется изменчивостью . То, как мы это делаем, часто может привести к неожиданным ошибкам в наших проектах. Если ваш код имеет дело только с примитивными типами данных (числа, строки, логические значения), вам не нужно беспокоиться. Однако, если вы работаете с массивами и объектами, выполнение изменяемых операций над ними может привести к неожиданным ошибкам. Чтобы продемонстрировать это, откройте свой терминал и запустите интерактивную оболочку Node:
node
Далее, давайте создадим массив, а затем назначим его другой переменной:
> let a = [1,2,3] > let b = a > b.push(9) > console.log(b) [ 1, 2, 3, 9 ] // b output > console.log(a) [ 1, 2, 3, 9 ] // a output
Как видите, обновление array b
привело к изменению array a
. Это происходит потому, что объекты и массивы являются известными ссылочными типами данных. Это означает, что такие типы данных на самом деле не содержат сами значения, а являются указателями на область памяти, где хранятся значения. Присваивая a
b
, мы просто создали второй указатель, который ссылается на то же место. Чтобы это исправить, нам нужно скопировать ссылочные значения в новое место. В JavaScript есть три различных способа достижения этого:
- используя неизменяемые структуры данных, созданные Immutable.js
- использование библиотек JavaScript, таких как Underscore и Lodash, для выполнения неизменяемых операций
- использование собственных функций ES6 для выполнения неизменяемых операций.
В этой статье мы будем использовать способ ES6 , поскольку он уже доступен в среде NodeJS. Внутри терминала NodeJS
выполните следующее:
> a = [1,2,3] // reset a [ 1, 2, 3 ] > b = Object.assign([],a) // copy array a to b [ 1, 2, 3 ] > b.push(8) > console.log(b) [ 1, 2, 3, 8 ] // b output > console.log(a) [ 1, 2, 3 ] // a output
В приведенном выше примере кода массив b
теперь можно изменить, не затрагивая массив a
. Мы использовали Object.assign () для создания новой копии значений, на которые теперь будет указывать переменная b
. Мы также можем использовать rest operator(...)
для выполнения неизменяемой операции, например:
> a = [1,2,3] [ 1, 2, 3 ] > b = [...a, 4, 5, 6] [ 1, 2, 3, 4, 5, 6 ] > a [ 1, 2, 3 ]
Оператор rest также работает с объектными литералами! Я не буду углубляться в эту тему, но вот некоторые дополнительные функции ES6, которые мы будем использовать для выполнения неизменяемых операций:
- синтаксис распространения — полезен в операциях добавления
- функция карты — полезна в операции обновления
- функция фильтра — полезна в операции удаления
Если документация, которую я привел, бесполезна, не беспокойтесь, так как вы увидите, как они используются на практике. Давайте начнем кодировать!
Настройка Redux
Самый быстрый способ настроить среду разработки Redux — использовать инструмент create-react-app
. Прежде чем мы начнем, убедитесь, что вы установили и обновили nodejs
, npm
и nodejs
. Давайте настроим проект Redux, сгенерировав проект redux-shopping-cart
и установив пакет Redux :
create-react-app redux-shopping-cart cd redux-shopping-cart yarn add redux # or npm install redux
Удалите все файлы в папке src
кроме index.js
. Откройте файл и очистите весь существующий код. Введите следующее:
import { createStore } from "redux"; const reducer = function(state, action) { return state; } const store = createStore(reducer);
Позвольте мне объяснить, что делает приведенный выше фрагмент кода:
- 1-е утверждение Мы импортируем функцию
createStore()
из пакета Redux. - 2-е утверждение . Мы создаем пустую функцию, известную как редуктор . Первый аргумент,
state
, это текущие данные, хранящиеся в хранилище. Второй аргумент,action
, является контейнером для:- тип — простая строковая константа, например,
ADD
,UPDATE
,DELETE
и т. д. - полезная нагрузка — данные для обновления состояния
- тип — простая строковая константа, например,
- 3-е утверждение . Мы создаем хранилище Redux, которое может быть построено только с использованием редуктора в качестве параметра. Доступ к данным, хранящимся в хранилище Redux, возможен напрямую, но может быть обновлен только через поставляемый редуктор.
Возможно, вы заметили, что я упомянул текущие данные, как будто они уже существуют. В настоящее время наше state
не определено или равно нулю. Чтобы исправить это, просто назначьте значение по умолчанию для состояния, подобного этому, чтобы сделать его пустым массивом:
const reducer = function(state=[], action) { return state; }
Теперь давайте практиковаться. Созданный нами редуктор является универсальным. Его название не описывает, для чего оно. Тогда есть проблема того, как мы работаем с несколькими редукторами. Ответ заключается в том, чтобы использовать функцию combineReducers
которая поставляется пакетом Redux. Обновите ваш код следующим образом:
// src/index.js … import { combineReducers } from 'redux'; const productsReducer = function(state=[], action) { return state; } const cartReducer = function(state=[], action) { return state; } const allReducers = { products: productsReducer, shoppingCart: cartReducer } const rootReducer = combineReducers(allReducers); let store = createStore(rootReducer);
В приведенном выше коде мы переименовали универсальный редуктор в cartReducer
. Существует также новый пустой редуктор с именем productsReducer
который я создал, чтобы показать вам, как объединить несколько редукторов в одном хранилище с combineReducers
функции combineReducers
.
Далее мы рассмотрим, как мы можем определить некоторые тестовые данные для наших редукторов. Обновите код следующим образом:
// src/index.js … const initialState = { cart: [ { product: 'bread 700g', quantity: 2, unitCost: 90 }, { product: 'milk 500ml', quantity: 1, unitCost: 47 } ] } const cartReducer = function(state=initialState, action) { return state; } … let store = createStore(rootReducer); console.log("initial state: ", store.getState());
Просто чтобы подтвердить, что в хранилище есть некоторые начальные данные, мы используем store.getState()
чтобы распечатать текущее состояние в консоли. Вы можете запустить dev-сервер, выполнив npm start
или yarn start
в консоли. Затем нажмите Ctrl+Shift+I
чтобы открыть вкладку инспектора в Chrome и просмотреть вкладку консоли.
Рисунок 2: Исходное состояние Redux
В настоящее время наш cartReducer
ничего не делает, но он должен управлять состоянием наших элементов корзины в магазине Redux. Нам нужно определить действия для добавления, обновления и удаления элементов корзины. Давайте начнем с определения логики для действия ADD_TO_CART
:
// src/index.js … const ADD_TO_CART = 'ADD_TO_CART'; const cartReducer = function(state=initialState, action) { switch (action.type) { case ADD_TO_CART: { return { ...state, cart: [...state.cart, action.payload] } } default: return state; } } …
Не торопитесь, чтобы проанализировать и понять код. Ожидается, что редуктор будет обрабатывать различные типы действий, следовательно, необходим оператор SWITCH
. Когда действие типа ADD_TO_CART
отправляется в любое место приложения, определенный здесь код будет обрабатывать его. Как видите, мы используем информацию, представленную в action.payload
чтобы объединить существующее состояние для создания нового состояния.
Далее мы определим action
, которое необходимо в качестве параметра для store.dispatch()
. Действия — это просто объекты JavaScript, которые должны иметь type
и дополнительную полезную нагрузку. Давайте продолжим и определим один сразу после функции cartReducer
:
… function addToCart(product, quantity, unitCost) { return { type: ADD_TO_CART, payload: { product, quantity, unitCost } } } …
Здесь мы определили функцию, которая возвращает простой объект JavaScript. Ничего фантастического. Прежде чем отправлять, давайте добавим код, который позволит нам прослушивать изменения событий в хранилище. Поместите этот код сразу после оператора console.log()
:
… let unsubscribe = store.subscribe(() => console.log(store.getState()) ); unsubscribe();
Далее, давайте добавим несколько товаров в корзину, отправив действия в магазин. Поместите этот код перед unsubscribe()
:
… store.dispatch(addToCart('Coffee 500gm', 1, 250)); store.dispatch(addToCart('Flour 1kg', 2, 110)); store.dispatch(addToCart('Juice 2L', 1, 250));
Для пояснения ниже я проиллюстрирую, как должен выглядеть весь код после внесения всех вышеуказанных изменений:
// src/index.js import { createStore } from "redux"; import { combineReducers } from 'redux'; const productsReducer = function(state=[], action) { return state; } const initialState = { cart: [ { product: 'bread 700g', quantity: 2, unitCost: 90 }, { product: 'milk 500ml', quantity: 1, unitCost: 47 } ] } const ADD_TO_CART = 'ADD_TO_CART'; const cartReducer = function(state=initialState, action) { switch (action.type) { case ADD_TO_CART: { return { ...state, cart: [...state.cart, action.payload] } } default: return state; } } function addToCart(product, quantity, unitCost) { return { type: ADD_TO_CART, payload: { product, quantity, unitCost } } } const allReducers = { products: productsReducer, shoppingCart: cartReducer } const rootReducer = combineReducers(allReducers); let store = createStore(rootReducer); console.log("initial state: ", store.getState()); let unsubscribe = store.subscribe(() => console.log(store.getState()) ); store.dispatch(addToCart('Coffee 500gm', 1, 250)); store.dispatch(addToCart('Flour 1kg', 2, 110)); store.dispatch(addToCart('Juice 2L', 1, 250)); unsubscribe();
После сохранения кода Chrome автоматически обновится. Проверьте вкладку консоли, чтобы убедиться, что новые элементы были добавлены:
Рисунок 3: Действия Redux отправлены
Организация кода Redux
Файл index.js
быстро стал большим. Это не то, как код Redux написан. Я только сделал это, чтобы показать вам, насколько прост Redux. Давайте посмотрим, как должен быть организован проект Redux. Сначала создайте следующие папки и файлы в папке src
, как показано ниже:
src/ ├── actions │ └── cart-actions.js ├── index.js ├── reducers │ ├── cart-reducer.js │ ├── index.js │ └── products-reducer.js └── store.js
Далее давайте начнем перемещать код из index.js
в соответствующие файлы:
// src/actions/cart-actions.js export const ADD_TO_CART = 'ADD_TO_CART'; export function addToCart(product, quantity, unitCost) { return { type: ADD_TO_CART, payload: { product, quantity, unitCost } } }
// src/reducers/products-reducer.js export default function(state=[], action) { return state; }
// src/reducers/cart-reducer.js import { ADD_TO_CART } from '../actions/cart-actions'; const initialState = { cart: [ { product: 'bread 700g', quantity: 2, unitCost: 90 }, { product: 'milk 500ml', quantity: 1, unitCost: 47 } ] } export default function(state=initialState, action) { switch (action.type) { case ADD_TO_CART: { return { ...state, cart: [...state.cart, action.payload] } } default: return state; } }
// src/reducers/index.js import { combineReducers } from 'redux'; import productsReducer from './products-reducer'; import cartReducer from './cart-reducer'; const allReducers = { products: productsReducer, shoppingCart: cartReducer } const rootReducer = combineReducers(allReducers); export default rootReducer;
// src/store.js import { createStore } from "redux"; import rootReducer from './reducers'; let store = createStore(rootReducer); export default store;
// src/index.js import store from './store.js'; import { addToCart } from './actions/cart-actions'; console.log("initial state: ", store.getState()); let unsubscribe = store.subscribe(() => console.log(store.getState()) ); store.dispatch(addToCart('Coffee 500gm', 1, 250)); store.dispatch(addToCart('Flour 1kg', 2, 110)); store.dispatch(addToCart('Juice 2L', 1, 250)); unsubscribe();
После того, как вы закончили обновление кода, приложение должно работать как раньше, теперь оно лучше организовано. Давайте теперь посмотрим, как мы можем обновлять и удалять элементы из корзины. Откройте cart-reducer.js
и обновите код следующим образом:
// src/reducers/cart-actions.js … export const UPDATE_CART = 'UPDATE_CART'; export const DELETE_FROM_CART = 'DELETE_FROM_CART'; … export function updateCart(product, quantity, unitCost) { return { type: UPDATE_CART, payload: { product, quantity, unitCost } } } export function deleteFromCart(product) { return { type: DELETE_FROM_CART, payload: { product } } }
Затем обновите cart-reducer.js
следующим образом:
// src/reducers/cart-reducer.js … export default function(state=initialState, action) { switch (action.type) { case ADD_TO_CART: { return { ...state, cart: [...state.cart, action.payload] } } case UPDATE_CART: { return { ...state, cart: state.cart.map(item => item.product === action.payload.product ? action.payload : item) } } case DELETE_FROM_CART: { return { ...state, cart: state.cart.filter(item => item.product !== action.payload.product) } } default: return state; } }
Наконец, давайте UPDATE_CART
действия UPDATE_CART
и DELETE_FROM_CART
в index.js
:
// src/index.js … // Update Cart store.dispatch(updateCart('Flour 1kg', 5, 110)); // Delete from Cart store.dispatch(deleteFromCart('Coffee 500gm')); …
Ваш браузер должен автоматически обновиться после сохранения всех изменений. Проверьте вкладку консоли, чтобы подтвердить результаты:
Рисунок 4: Обновление и удаление действий Redux
Как подтверждено, количество на 1 кг муки обновляется с 2 до 5, а 500 г кофе удаляется из корзины.
Отладка с помощью инструментов Redux
Теперь, если мы допустили ошибку в нашем коде, как мы отлаживаем проект Redux?
Redux поставляется с множеством сторонних средств отладки, которые мы можем использовать для анализа поведения кода и исправления ошибок. Вероятно, наиболее популярным из них является инструмент для путешествий во времени , иначе известный как redux-devtools-extension . Настройка это трехэтапный процесс. Сначала перейдите в браузер Chrome и установите расширение Redux Devtools .
Рисунок 5: Redux DevTools Chrome Extensions
Затем перейдите к терминалу, на котором работает приложение Redux, и нажмите Ctrl+C
чтобы остановить сервер разработки. Затем используйте npm или yarn для установки пакета redux-devtools-extension . Лично я предпочитаю Yarn, так как есть файл yarn.lock
который я бы хотел обновлять.
yarn add redux-devtools-extension
После завершения установки вы можете запустить сервер разработки, поскольку мы реализуем последний этап внедрения инструмента. Откройте store.js
и замените существующий код следующим образом:
// src/store.js import { createStore } from "redux"; import { composeWithDevTools } from 'redux-devtools-extension'; import rootReducer from './reducers'; const store = createStore(rootReducer, composeWithDevTools()); export default store;
Не стесняйтесь обновлять src/index.js
и удалять весь код, связанный с регистрацией на консоли и подпиской на магазин. Это больше не нужно. Теперь вернитесь в Chrome и откройте панель Redux DevTools, щелкнув правой кнопкой мыши значок инструмента:
Рисунок 6: Redux DevTools Menu
В моем случае я выбрал вариант « Снизу» . Не стесняйтесь попробовать другие варианты.
Рисунок 7: Панель Redux DevTools
Как видите, Redux Devtool просто потрясающий. Вы можете переключаться между методами действий, состояний и различий. Выберите действия на левой панели и посмотрите, как меняется дерево состояний. Вы также можете использовать ползунок для воспроизведения последовательности действий. Вы даже можете отправить прямо из инструмента! Ознакомьтесь с документацией, чтобы узнать больше о том, как вы можете настроить инструмент в соответствии со своими потребностями.
Интеграция с React
В начале этого урока я упоминал, что Redux действительно хорошо сочетается с React. Ну, вам нужно всего лишь несколько шагов для настройки интеграции. Во-первых, остановите сервер разработки, так как нам нужно будет установить пакетact-redux , официальные привязки Redux для React:
yarn add react-redux
Затем обновите index.js
чтобы включить код React. Мы также будем использовать класс Provider
чтобы обернуть приложение React в контейнер Redux:
// src/index.js … import React from 'react'; import ReactDOM from 'react-dom'; import { Provider } from 'react-redux'; const App = <h1>Redux Shopping Cart</h1>; ReactDOM.render( <Provider store={store}> { App } </Provider> , document.getElementById('root') ); …
Точно так же мы завершили первую часть интеграции. Теперь вы можете запустить сервер, чтобы увидеть результат. Вторая часть включает в себя связывание компонентов React с хранилищем Redux и действиями с использованием пары функций, предоставляемых только что установленным нами react-redux
пакетом. Кроме того, вам нужно настроить API с помощью Express или инфраструктуры, такой как Feathers . API предоставит нашему приложению доступ к службе базы данных.
В Redux нам также необходимо установить дополнительные пакеты, такие как axios
для выполнения запросов API через действия Redux. После этого наше состояние компонентов React будет обрабатываться Redux, обеспечивая синхронизацию всех компонентов с API базы данных. Чтобы узнать больше о том, как добиться всего этого, ознакомьтесь с другим моим учебником « Создание приложения CRUD с использованием React, Redux и FeathersJS ».
Резюме
Я надеюсь, что это руководство дало вам полезное введение в Redux. Тем не менее, есть еще кое-что для вас, чтобы учиться. Например, вам нужно научиться работать с асинхронными действиями, аутентификацией, ведением журнала, обработкой форм и так далее. Теперь, когда вы знаете, что такое Redux, вам будет проще опробовать другие подобные фреймворки, такие как Flux , Alt.js или Mobx . Если вы считаете, что Redux вам подходит, я настоятельно рекомендую следующие уроки, которые помогут вам получить еще больше опыта в Redux: