Приступая к работе с Vuex: руководство для начинающих
Vuex является официальным решением для управления состоянием Vue. Этот шаблон проектирования использует централизованное хранилище, предоставляя компонентам методы для доступа к данным состояния.
В этой статье я приведу общий обзор Vuex и продемонстрирую, как использовать его в простом приложении.
Пример корзины покупок
В интернет-магазине у каждого товара есть кнопка «Добавить в корзину», а также метка «В наличии» с указанием текущего запаса. Каждый раз, когда товар приобретается, запас уменьшается. Когда это происходит, значение доступного остатка должно обновляться.
Когда товар закончится, возле товара должна появиться метка «Нет на складе», а кнопка «Добавить в корзину» должна быть отключена или скрыта.
Теперь представим, что вам нужно разработать API, который позволяет сторонним сайтам продавать товары напрямую со склада. Этот API должен обеспечивать синхронизацию остатков товара, отображаемых на основном сайте интернет-магазина. Вот где библиотека шаблонов управления состоянием избавит вас от проблем. Она поможет организовать код, который обрабатывает входные данные так, чтобы упростить добавление новых условий.
Что нужно?
- Базовые познания в Vue.js;
- Знания функций редакции JS ES6 и ES7.
Вам также потребуется последняя версия Node.js и Vue CLI. Чтобы установить среду, выполните следующую команду:
npm install -g @vue/cli
Создание счетчика, используя локальное состояние
Мы создадим простой счетчик, который локально отслеживает состояние. Для этого сгенерируем новый проект, используя CLI:
vue create vuex-counter
Откроется мастер, который проведет вас через процесс создания проекта. Выберите пункт «Manually select features» и установите Vuex. Затем перейдите в созданный каталог и в папке :src/components
переименуйте файл HelloWorld.vue
в Counter.vue
.
cd vuex-counter
mv src/components/HelloWorld.vue src/components/Counter.vue
Затем откройте файл :src/App.vue
и замените существующий код следующим:
<template>
<div id="app">
<h1>Vuex Counter</h1>
<Counter/>
</div>
</template>
<script>
import Counter from './components/Counter.vue'
export default {
name: 'app',
components: {
Counter
}
}
</script>
Базовые стили можно оставить без изменений.
Создание счетчика
Начнем с инициализации счетчика и его вывода на странице. Мы также сообщим пользователю, является ли сейчас значение счетчика четным или нечетным.
Откройте файл src/components/Counter.vue
и замените его код на следующий:
<template>
<div>
<p>Clicked {{ count }} times! Count is {{ parity }}.</p>
</div>
</template>
<script>
export default {
name: 'Counter',
data: function() {
return {
count: 0
};
},
computed: {
parity: function() {
return this.count % 2 === 0 ? 'even' : 'odd';
}
}
}
</script>
У нас есть одна переменная состояния с именем count
и вычисляемая функция parity
. Она возвращает строку even
или odd
в зависимости от того, является ли значение count
четным или нечетным.
Запустите приложение из корневой папки с помощью команды npm run serve
и перейдите по адресу http://localhost: 8080. Попробуйте изменить значение счетчика, чтобы убедиться, что для counter
и parity
отображается правильный вывод.
Увеличение и уменьшение
Сразу после свойства computed
в разделе <script>
файла Counter.vue
добавьте следующий код:
methods: {
increment: function () {
this.count++;
},
decrement: function () {
this.count--;
},
incrementIfOdd: function () {
if (this.parity === 'odd') {
this.increment();
}
},
incrementAsync: function () {
setTimeout(() => {
this.increment()
}, 1000)
}
}
Имена первых двух функций (increment
и decrement
) говорят сами за себя. Функция incrementIfOdd
выполняется только, если значение count
является нечетным.
incrementAsync
является асинхронной функцией, которая увеличивает значение на единицу через одну секунду.
Чтобы получить доступ к этим новым методам из шаблона, нужно определить несколько кнопок. Вставьте следующий код после шаблона, который выводит количество и четность:
<button @click="increment" variant="success">Increment</button>
<button @click="decrement" variant="danger">Decrement</button>
<button @click="incrementIfOdd" variant="info">Increment if Odd</button>
<button @click="incrementAsync" variant="warning">Increment Async</button>
После сохранения изменений они должны автоматически отобразиться в браузере. Нажмите на все кнопки, чтобы убедиться в их работоспособности.
Как работает Vuex
Прежде чем перейти к практической реализации, разберемся в том, как организован код Vuex.
Хранилище Vuex
Инструмент предоставляет централизованное хранилище для общего состояния приложений Vue. Вот как это выглядит в базовой форме:
// src/store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
// помещаем сюда переменные и собираем их
},
mutations: {
// помещаем сюда синхронные функции для изменения состояния: добавления, редактирования, деления
},
actions: {
// помещаем сюда асинхронные функции, которые могут вызывать одну или несколько функций мутации
}
})
После определения хранилища добавляем его в приложение Vue.js следующим образом:
// src/main.js
import store from './store'
new Vue({
store,
render: h => h(App)
}).$mount('#app')
Это сделает экземпляр хранилища доступным для каждого компонента в приложении через this.$store
.
Работа с состоянием
Vuex работает с одним хранилищем. Данные приложения организованы в древовидную структуру. Ее схема довольно проста. Вот пример:
state: {
products: [],
count: 5,
loggedInUser: {
name: 'John',
role: 'Admin'
}
}
Переменная products
– это пустой массив. Переменной ≈ инициализируется значением 5. loggedInUser
– это литерал JavaScript-объекта, содержащий несколько полей.
Свойства состояния могут содержать любой допустимый тип данных от логических значений до массивов и других объектов. Есть несколько способов отображения состояния в представлениях. Можно ссылаться на хранилище непосредственно в шаблонах, используя $store
:
<template>
<p>{{ $store.state.count }}</p>
</template>
Или можем вернуть состояние хранилища из вычисляемого свойства:
<template>
<p>{{ count }}</p>
</template>
<script>
export default {
computed: {
count() {
return this.$store.state.count;
}
}
}
</script>
Хранилища Vuex являются реактивными, поэтому при изменении значения $store.state.count
также изменяется представление.
Вспомогательная функция mapState
Предположим, что есть несколько состояний, которые нужно отобразить. Для этого Vuex предоставляет вспомогательную функцию mapState. Ее можно использовать для простого создания нескольких вычисляемых свойств. Вот пример:
<template>
<div>
<p>Welcome, {{ loggedInUser.name }}.</p>
<p>Count is {{ count }}.</p>
</div>
</template>
<script>
import { mapState } from 'vuex';
export default {
computed: mapState({
count: state => state.count,
loggedInUser: state => state.loggedInUser
})
}
</script>
Также функции mapState можно передать массив строк:
export default {
computed: mapState([
'count', 'loggedInUser'
])
}
Обратите внимание, что mapState
возвращает объект. Если нужно использовать функцию с другими вычисляемыми свойствами, то можете применить оператор распространения:
computed: {
...mapState([
'count', 'loggedInUser'
]),
parity: function() {
return this.count % 2 === 0 ? 'even' : 'odd'
}
}
Геттеры
В хранилище Vuex геттеры эквивалентны вычисляемым свойствам Vue. Они позволяют создавать производные состояния, которые могут быть разделены между различными компонентами. Пример:
getters: {
depletedProducts: state => {
return state.products.filter(product => product.stock <= 0)
}
}
Результаты работы хэндлеров getter
(при обращении к ним, как к свойствам) кэшируются и могут вызываться сколько угодно раз. Они также реагируют на изменения состояния. Если состояние зависит от изменений getter
, функция выполняется автоматически, и новый результат кэшируется.
Любой компонент, который получил доступ к обработчику getter
, будет немедленно обновлен. Пример того, как получить доступ к обработчику getter
из компонента:
computed: {
depletedProducts() {
return this.$store.getters.depletedProducts;
}
}
Вспомогательная функция mapGetters
Вы можете упростить код геттеров с помощью вспомогательной функции mapGetters
:
import { mapGetters } from 'vuex'
export default {
//..
computed: {
...mapGetters([
'depletedProducts',
'anotherGetter'
])
}
}
Функция может передавать аргументы обработчику getter
. Это полезно, если вы хотите выполнить запрос в пределах getter
:
getters: {
getProductById: state => id => {
return state.products.find(product => product.id === id);
}
}
store.getters.getProductById(5)
Каждый раз, как к обработчику getter
обращаются через метод, он всегда запускается, и результат не будет кэшироваться.
Для сравнения:
// использование свойства, результат кэшируется
store.getters.depletedProducts
// использование метода, результат не кэшируется
store.getters.getProductById(5)
Изменение состояния с помощью мутаций
В Vuex компоненты никогда не изменяют состояние напрямую. Это может привести к ошибкам. Поэтому лучше использовать мутации. Вот пример мутации, которая увеличивает переменную count
, хранящуюся в state
:
export default new Vuex.Store({
state:{
count: 1
},
mutations: {
increment(state) {
state.count++
}
}
})
Вы не можете вызвать обработчик мутаций напрямую. Вместо этого вы запускаете его, «совершая мутацию»:
methods: {
updateCount() {
this.$store.commit('increment');
}
}
Вы также можете передать в мутацию параметры:
// store.js
mutations: {
incrementBy(state, n) {
state.count += n;
}
}
// компонент
updateCount() {
this.$store.commit('incrementBy', 25);
}
В приведенном выше примере мы передаем мутации целое число, на которое она должна увеличить счетчик. Но так же можно передать объект в качестве параметра. Так вы можете легко включить несколько полей, не перегружая обработчик мутаций:
// store.js
mutations: {
incrementBy(state, payload) {
state.count += payload.amount;
}
}
// компонент
updateCount() {
this.$store.commit('incrementBy', { amount: 25 });
}
Вы также можете выполнить изменение состояния в стиле объекта:
store.commit({
type: 'incrementBy',
amount: 25
})
Обработчик мутаций останется прежним.
Вспомогательная функция mapMutations
Чтобы уменьшить объем кода для обработчиков мутаций, можно использовать вспомогательную функцию mapMutations
:
import { mapMutations } from 'vuex'
export default{
methods: {
...mapMutations([
'increment', // сопоставляем с this.increment()
'incrementBy' // сопоставляем с this.incrementBy(amount)
])
}
}
Действия
Действия - это функции, которые не меняют состояние. Они совершают мутации после выполнения некоторой логики (которая часто является асинхронной). Простой пример действия:
//..
actions: {
increment(context) {
context.commit('increment');
}
}
Обработчики действий получают объект context
в качестве первого аргумента, который предоставляет доступ к свойствам и методам хранилища. Например:
context.commit
: совершить мутацию;context.state
: получить доступ к состоянию;context.getters
: получить доступ к геттерам;
Вы также можете использовать деструктуризацию аргументов для извлечения атрибутов хранилища. Например:
actions: {
increment({ commit }) {
commit('increment');
}
}
Действия могут быть асинхронными. Вот пример:
actions: {
incrementAsync: async({ commit }) => {
return await setTimeout(() => { commit('increment') }, 1000);
}
}
В этом примере мутация выполняется через 1000 миллисекунд. Обработчики действий вызываются не напрямую, а через специальный метод dispatch
в хранилище:
store.dispatch('incrementAsync')
// dispatch с нагрузкой
store.dispatch('incrementBy', { amount: 25})
// dispatch с объектом
store.dispatch({
type: 'incrementBy',
amount: 25
})
Вы можете отправить действие в компонент следующим образом:
this.$store.dispatch('increment')
Вспомогательная функция mapActions
Также вы можете использовать вспомогательную функцию mapActions
для назначения обработчиков действий локальным методам:
import { mapActions } from 'vuex'
export default {
//..
methods: {
...mapActions([
'incrementBy', // сопоставляем this.increment(amount) с this.$store.dispatch(increment)
'incrementAsync', // сопоставляем this.incrementAsync() с this.$store.dispatch(incrementAsync)
add: 'increment' // сопоставляем this.add() с this.$store.dispatch(increment)
])
}
}
Реализация приложения счетчика с помощью Vuex
Чтобы модифицировать приложение счетчика с «локальным состоянием» в приложение Vuex, откройте файл src/store.js
и обновите код следующим образом:
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
count: 0
},
getters: {
parity: state => state.count % 2 === 0 ? 'even' : 'odd'
},
mutations: {
increment(state) {
state.count++;
},
decrement(state) {
state.count--;
}
},
actions: {
increment: ({ commit }) => commit('increment'),
decrement: ({ commit }) => commit('decrement'),
incrementIfOdd: ({ commit, getters }) => getters.parity === 'odd' ? commit('increment') : false,
incrementAsync: ({ commit }) => {
setTimeout(() => { commit('increment') }, 1000);
}
}
});
Здесь мы видим, как на практике структурируется хранилище Vuex.
Обновите компонент src/components/Counter.vue
, заменив код в блоке <script>
. Мы переключим локальное состояние и функции на вновь созданные в хранилище Vuex:
import { mapState mapGetters, mapActions } from 'vuex'
export default {
name: 'Counter',
computed: {
...mapState([
'count'
]),
...mapGetters([
'parity'
])
},
methods: mapActions([
'increment',
'decrement',
'incrementIfOdd',
'incrementAsync'
])
}
Код шаблона должен остаться прежним. Обратите внимание, насколько чище стал код.
Если вы не хотите использовать вспомогательные функции состояния, то можете получить доступ к данным хранилища прямо из шаблона:
<p>
Clicked {{ $store.state.count }} times! Count is {{ $store.getters.parity }}.
</p>
После сохранения внесенных изменений обязательно протестируйте приложение.
Заключение
Из этой статьи мы узнали, что такое Vuex, какую проблему он решает и как его установить. Затем мы применили эти концепции для модификации приложения-счетчика с помощью Vuex.