Vuex — глубокое погружение
Эта статья посвящена Vuex, библиотеке управления состоянием Vue.js. Мы рассмотрим проблемы, которые решает эта библиотека, основные концепции ее архитектуры, как ее настроить и использовать.
Vuex — это библиотека управления состоянием, созданная командой Vue для управления данными в приложениях Vue.js. Она обеспечивает централизованный способ управления данными, которые используются в приложении, и позволяет легко ими манипулировать.
Почему Vuex?
Vue способствует разделению представлений на компоненты. Эти компоненты могут многократно использоваться экземплярами Vue, которые принимают данные, методы и т. д. В данных хранится состояние представления, а методы позволяют манипулировать этим состоянием на основе взаимодействий пользователя с представлением.
Когда пользователь нажимает кнопку в компоненте, вызывается метод, который, в свою очередь, выполняет действие над указанным состоянием, в то время как упомянутое состояние обновляет свое представление.
Однако бывают случаи, когда нескольким компонентам необходимо совместно использовать состояние, или после изменения состояния в одном компоненте вам необходим родительский или дочерний компонент для выполнения последующего действия.
В зависимости от положения этого второго компонента, вы можете решить эту задачу использовав либо props, либо this.$parent для прямого доступа к данным или методам второго компонента. Но что, если вам нужно сделать это для большого количества компонентов?
По мере того как проект увеличивается в размерах, вы обнаруживаете, что передаете очень большое количество props и начинается в большом количестве непосредственно манипулировать DOM для доступа к различным компонентам.
Такой подход становится очень утомительным, а также затрудняет поддержку или отладку кода при возникновении ошибок. Для решения этой проблемы и предназначен Vuex. Он создает глобальную область (хранилище), в которую можно поместить все состояния, которые будут совместно использоваться различными компонентами.
Он также делает наш код более структурированным, облегчает отладку, поскольку мы можем использовать DevTools для отслеживания возникающих ошибок, и, конечно, обеспечивает реактивность, на которой и построен Vue. Вы можете представить аналогию с Vuex как о глобальным объекте window в JavaScript — к которому каждый компонент имеет доступ.
Инсталяция Vuex
Чтобы установить Vuex в проект, запустите код ниже.
npm install vuex --save
Эта команда установит последнюю версию Vuex. Как только это будет сделано, нам нужно инициализировать Vuex в приложении Vue, создав наш файл store.js с кодом ниже;
import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex)
Теперь мы можем приступить к созданию хранилища (store). Хранилище по сути является реактивным объектом, который содержит состояние приложения (state), геттеры (getters), мутации (mutations) и действия (actions).
Хранилище (Store)
Хранилище — это, по сути, централизованное состояние, в котором есть некоторые основные концепции, которые позволяют нам добиться этой централизации. Эти концепции:
- State (состояние)
- Getters (геттеры)
- Mutations (мутации)
- Actions (действия)
State
Это один объект, который содержит все данные. Это похоже на ключевое слово data в структуре отдельных компонентов, за исключением того, что к этому состоянию можно получить доступ более чем из одного компонента, и, когда это состояние обновляется, все компоненты, обращающиеся к нему, также получают это изменение. Что бы его создать, нужно сделать следующее:
// import Vue import Vue from 'vue'; // import Vuex import Vuex from 'vuex'; // Install the Vuex plugin on vue Vue.use(Vuex); // create a Vuex store instance export const store = new Vuex.Store({ state: { cart: '' } })
Чтобы получить доступ к состоянию Vuex в наших компонентах Vue, нам необходимо сначала импортировать хранилище в компонент, создав вычисляемое свойство, которое будет возвращать указанное состояние, а затем отобразить указанное состояние в представлении.
Теперь давайте импортируем хранилище. Есть два основных способа сделать это:
1. Импортируйте хранилище вручную в каждый компонент, для которого вы собираетесь использовать состояние Vuex, например:
template> <main> <h1>Cart Content</h1> <p>{{cartValue}}</p> </main> </template> <script> // Import Vuex Store into Component import store from 'store.js'; export default { computed: { cartValue() { // Return Vuex state from store return store.state.cart; } } } </script>
2. Внедрение глобального хранилища Vuex в Vue Instance, которое автоматически дает нам доступ к хранилищу из всех компонентов Vue в приложении с использованием синтаксиса this.$store:
import Vue from 'vue'; import store from './store.js'; new Vue({ // Adding the Vuex store to the Vue instance store, }).$mount('#app');
<template> <main> <h1>Cart Content</h1> <p>{{cartValue}}</p> </main> </template> <script> export default { computed: { cartValue() { // Accessing the Vuex state return this.$store.state.cart; } } } </script>
Getters
Геттеры (Getters) являются в значительной степени вычисляемыми свойствами для хранилища Vuex. Они позволяют нам генерировать новое состояние на основе текущего состояния — например, вычисляя, сколько товаров у нас в корзине.
Это также помогает в сокращении дублирования кода, когда в идеале эти данные нужны более чем одному компоненту, и мы обычно должны выполнять наши манипуляции в каждом компоненте. С геттерами мы можем сделать это один раз и ссылаться на них где угодно.
Чтобы создать геттер, мы делаем следующее:
// import Vue import Vue from 'vue'; // import Vuex import Vuex from 'vuex'; // Install the Vuex plugin on vue Vue.use(Vuex); // create a Vuex store instance export const store = new Vuex.Store({ state: { cart: ["bread", "rice", "beans", "turkey"] }, getters: { // Fetch the total number of items in the cart totalNumberOfCartItems: state => { return state.cart.length; }, }, })
Далее мы получаем доступ к геттеру из нашего компонента Vue, выполнив следующие действия:
<template> <main> <h1>Cart Content</h1> <p>Total Number of Items: {{totalNumberOfCartItems}}</p> </main> </template> <script> export default { computed: { totalNumberOfCartItems() { // Accessing the Vuex state return this.$store.getters.totalNumberOfCartItems; } } } </script>
Теперь, когда товар добавляется в корзину, общее количество товаров в корзине обновляется автоматически.
Mutations
Мутации — единственный способ, которым мы можем обновить наше состояние Vuex. Они выполняют одну и только одну задачу: установить состояние. Это функция, которая принимает два аргумента, состояние (state) и полезную нагрузку (payload), где полезная нагрузка не обязательна.
Полезная нагрузка (payload) — это просто данные, которые будут использоваться для обновления состояния. Мутации синхронны, и поэтому мы не можем выполнять асинхронные задачи в них.
Теперь давайте добавим мутацию в наш код:
// import Vue import Vue from 'vue'; // import Vuex import Vuex from 'vuex'; // Install the Vuex plugin on vue Vue.use(Vuex); // create a Vuex store instance export const store = new Vuex.Store({ state: { cart: ["bread", "rice", "beans", "turkey"] }, getters: { // Fetch the total number of items in the cart totalNumberOfCartItems: state => { return state.cart.length; }, }, mutations: { // Add item to cart addItemToCart (state, payload) { state.cart.push(payload); }, }, })
Далее нам нужно обновить состояние из нашего компонента Vue, и для этого нам нужно будет зафиксировать (commit) мутацию.
<template> <main> <h1>Cart Content</h1> <p>Total Number of Items: {{totalNumberOfCartItems}}</p> <form @submit.prevent="addItemToCart"> <input type="text" v-model="item" required> <button type="submit">Add to cart</button> </form> </main> </template> <script> export default { data() { return { item: '' } }, computed: { totalNumberOfCartItems() { // Accessing the Vuex state return this.$store.getters.totalNumberOfCartItems; } }, methods: { addItemToCart() { // Check that the input field isn't empty if(this.item !== '') { // commiting the additemtocart mutation with the payload this.$store.commit('addItemToCart', this.item) } } } } </script>
Теперь каждый раз, когда пользователь вводит значение в поле ввода и нажимает кнопку отправки, элемент добавляется в корзину, а общее количество элементов в корзине обновляется в представлении.
Actions
Действия (actions) похожи на мутации, но вместо того, чтобы мутировать состояние, они совершают (commit) мутации. Главное отличие действий от мутаций это то что действия асинхронны, то есть предназначены для выполнения асинхронных задач; когда эти задачи выполнены, нам нужно фиксировать (commit) мутацию, которая, в свою очередь, обновляет состояние.
Чтобы продемонстрировать действия, мы рассмотрим отправку элементов корзины в API.
// import Vue import Vue from 'vue'; // import Vuex import Vuex from 'vuex'; // Install the Vuex plugin on vue Vue.use(Vuex); // create a Vuex store instance export const store = new Vuex.Store({ state: { cart: ["bread", "rice", "beans", "turkey"] }, getters: { // Fetch the total number of items in the cart totalNumberOfCartItems: state => { return state.cart.length; }, }, mutations: { // Add item to cart addItemToCart (state, payload) { state.cart.push(payload); }, // Clear items in the cart emtpyCart (state) { state.cart = []; } }, actions: { checkout({commit}, requestObject) { // API Call to submit the items in the cart Vue.http.post('submit', requestObject).then((response) => { // log success console.log(response); // Clear Cart by mutating the state commit('emptyCart'); }).catch((error) => { // log error console.log(error); } } } })
Глядя на приведенный выше код, мы создали действие с именем checkout, которое принимает две вещи:
commit
: что позволит нам вызвать метод commit внутри наших действийrequestObject
: что позволит нам передать данные в действие
Внутри действия, мы сделали асинхронный вызов API, а затем передали requestObject в API. В случае успеха мы записали ответ, а затем приступили к очистке состояния корзины, но сначала нам нужно было создать мутацию emptyCart, единственная задача которой — очистить состояние корзины.
Теперь, когда мы рассмотрели, как создавать действия, мы рассмотрим запуск этого действия. Чтобы вызвать действие, Vuex предоставляет нам команду отправки (dispatch).
this.$store.dispatch('actionName', payload);
Давайте добавим действие в наш код и отправим его из представления:
<template> <main> <h1>Cart Content</h1> <p>Total Number of Items: {{totalNumberOfCartItems}}</p> <form @submit.prevent="addItemToCart"> <input type="text" v-model="item" required> <button type="submit">Add to cart</button> </form> <button type="button" @click="checkout">Checkout</button> </main> </template> <script> export default { data() { return { item: '' } }, computed: { totalNumberOfCartItems() { // Accessing the Vuex state return this.$store.getters.totalNumberOfCartItems; } }, methods: { addItemToCart() { // Check that the input field isn't empty if(this.item !== '') { // commiting the additemtocart mutation with the payload this.$store.commit('addItemToCart', this.item) } }, checkout() { // Make sure cart is not empty if(this.totalNumberOfCartItems > 0 ) { // create request let requestPayload = { cart: this.$store.state.cart }; // Dispatch the action this.$store.dispatch('checkout', requestPayload); } else { alert('Cart is empty'); } } } } </script>
На основе приведенного выше кода мы создали кнопку извлечения в представлении и создали метод извлечения, который проверяет, не пуста ли корзина, перед попыткой отправки действия, которое отправляет элементы.
Это работает, но чего-то не хватает. Вы можете спросить, что? Нам удалось отправить действие, но мы не знаем, было ли это действие успешным или нет.
Возможно вызов API не удался? Как я могу получить эту информацию, чтобы я мог уведомить пользователя? Действия могут обрабатывать Promises (Обещания), а также могут возвращать Promises.
Модифицируем наш пример кода для возврата Promise:
// import Vue import Vue from 'vue'; // import Vuex import Vuex from 'vuex'; // Install the Vuex plugin on vue Vue.use(Vuex); // create a Vuex store instance export const store = new Vuex.Store({ state: { cart: ["bread", "rice", "beans", "turkey"] }, getters: { // Fetch the total number of items in the cart totalNumberOfCartItems: state => { return state.cart.length; }, }, mutations: { // Add item to cart addItemToCart (state, payload) { state.cart.push(payload); }, // Clear items in the cart emtpyCart (state) { state.cart = []; } }, actions: { checkout({commit}, requestObject) { return new Promise((resolve, reject) => { // API Call to submit the items in the cart Vue.http.post('submit', requestObject).then((response) => { // log success console.log(response); // Clear Cart by mutating the state commit('emptyCart'); // return success resolve(response); }).catch((error) => { // log error console.log(error); // return error reject(error); } }) } } })
Теперь мы можем использовать возвращаемое значение, чтобы обновить пользователя о состоянии в представлении следующим образом:
<template> <main> <h1>Cart Content</h1> <p>Total Number of Items: {{totalNumberOfCartItems}}</p> <form @submit.prevent="addItemToCart"> <input type="text" v-model="item" required> <button type="submit">Add to cart</button> </form> <button type="button" @click="checkout">Checkout</button> </main> </template> <script> export default { data() { return { item: '' } }, computed: { totalNumberOfCartItems() { // Accessing the Vuex state return this.$store.getters.totalNumberOfCartItems; } }, methods: { addItemToCart() { // Check that the input field isn't empty if(this.item !== '') { // commiting the additemtocart mutation with the payload this.$store.commit('addItemToCart', this.item) } }, checkout() { // Make sure cart is not empty if(this.totalNumberOfCartItems > 0 ) { // create request let requestPayload = { cart: this.$store.state.cart }; // Dispatch the action this.$store.dispatch('checkout', requestPayload).then((response) => { // Alert Response from API alert(response); }).catch((error) => { // Alert Error from API alert(error); }); } else { alert('Cart is empty'); } } } } </script>
Actions (Действия) также позволяют вам отправлять несколько actions (то есть action может отправлять одно или несколько других actions). Все, что вам нужно сделать, это передать dispatch в качестве аргумента, и вы сможете отправлять другие action внутри вашего action.
checkout({ dispatch, commit }, requestObject) { // dispatch an action dispatch('actionName'); // dispatch another action dispatch('actionName2', request); };
Добавление структуры в хранилище
Прямо сейчас у нас есть все наши состояния, геттеры, мутации и действия в одном файле store.js. В зависимости от того, насколько велика наша кодовая база проекта, этот файл может стать очень большим, и обычно всегда имеет смысл разделить его на отдельные файлы.
store/ --| store.js --| state.js --| getters.js --| mutations.js --| actions.js
Теперь наше хранилище выглядит так:
import Vue from 'vue' import Vuex from 'vuex' import state from './state' import getters from './getters' import mutations from './mutations' import actions from './actions' Vue.use(Vuex) export default new Vuex.Store({ state, getters, mutations, actions })
Модули
Vuex также предоставляет нам модули, с помощью которых мы можем дополнительно структурировать или разбить наше хранилище на модули. Каждый модуль будет иметь свое собственное состояние, геттеры, мутации и действия.
Это работает путем группировки связанных состояний, геттеров, мутаций и действий в модуль. Это особенно полезно, когда у нас есть крупномасштабное приложение, а в хранилище много кода.
Реорганизовав наше хранилище в модуль, мы создадим файл cart.js и продолжим разбивать все наши состояния, мутации и действия в нашем хранилище, относящиеся к корзине, как показано ниже:
// import Vue import Vue from 'vue'; export default { state: { cart: ["bread", "rice", "beans", "turkey"] }, getters: { // Fetch the total number of items in the cart totalNumberOfCartItems: state => { return state.cart.length; }, }, mutations: { // Add item to cart addItemToCart (state, payload) { state.cart.push(payload); }, // Clear items in the cart emtpyCart (state) { state.cart = []; } }, actions: { checkout({commit}, requestObject) { return new Promise((resolve, reject) => { // API Call to submit the items in the cart Vue.http.post('submit', requestObject).then((response) => { // log success console.log(response); // Clear Cart by mutating the state commit('emptyCart'); // return success resolve(response); }).catch((error) => { // log error console.log(error); // return error reject(error); } }) } } }
Далее мы импортируем и регистрируем его в нашем главном хранилище.
import Vue from 'vue' import Vuex from 'vuex' import state from './state' import getters from './getters' import mutations from './mutations' import actions from './actions' import cart from './modules/cart' Vue.use(Vuex) export default new Vuex.Store({ state, getters, mutations, actions, modules: { cart } })
Наконец, наша структура кода будет выглядеть так:
store/ --| store.js --| state.js --| getters.js --| mutations.js --| actions.js --| modules/ --| cart.js
Заключение
Vuex создает хранилище, которое состоит из состояний, геттеров, мутаций и действий. Чтобы обновить или изменить состояние, вы должны совершить мутацию. Чтобы выполнить асинхронную задачу, вам нужно действие. Действия в случае успеха совершают мутацию, которая изменяет состояние, тем самым обновляя представление.
Оригинал: Nosa Obaseki Vuex—The Deep Dive
Ошибке по всему гексту.
Вы можете представиться аналогию с Vuex как о глобальным объекте window в JavaScript — к которому каждый компонент имеет доступ.