В одностраничных приложениях понятие состояния относится к любому фрагменту данных, который может измениться. Примером состояния могут быть сведения о вошедшем в систему пользователе или данные, полученные из API.
Обработка состояния в одностраничных приложениях может быть сложным процессом. По мере того, как приложение становится все больше и сложнее, вы начинаете сталкиваться с ситуациями, когда данный фрагмент состояния должен использоваться в нескольких компонентах, или вы обнаруживаете, что передаете состояние через компоненты, которые в нем не нуждаются, просто чтобы получить его там, где оно требуется. должно быть. Это также известно как «пропеллерное бурение», и может привести к некоторому громоздкому коду.
Vuex является официальным решением для государственного управления Vue. Он работает, имея центральное хранилище для общего состояния и предоставляя методы, позволяющие любому компоненту в вашем приложении получить доступ к этому состоянию. По сути, Vuex гарантирует, что ваши представления остаются согласованными с данными вашего приложения, независимо от того, какая функция вызывает изменение данных вашего приложения.
В этой статье я предложу вам общий обзор Vuex и продемонстрирую, как реализовать его в простом приложении.
Хотите узнать Vue.js с нуля? Получите полную коллекцию книг Vue, охватывающих основы, проекты, советы и инструменты и многое другое с SitePoint Premium. Присоединяйтесь сейчас всего за $ 9 / месяц .
Пример корзины покупок
Давайте рассмотрим реальный пример, чтобы продемонстрировать проблему, которую решает Vuex.
Когда вы заходите на сайт покупок, у вас обычно будет список товаров. У каждого товара есть кнопка « Добавить в корзину» , а иногда и ярлык « Оставшиеся товары» с указанием текущего запаса или максимального количества товаров, которое вы можете заказать для указанного товара. Каждый раз, когда продукт приобретается, текущий запас этого продукта уменьшается. Когда это происходит, метка « Оставшиеся элементы» должна обновляться с правильным значением. Когда уровень запаса продукта достигает 0, на этикетке должно появиться « Нет на складе» . Кроме того, кнопка « Добавить в корзину» должна быть отключена или скрыта, чтобы клиенты не могли заказывать товары, которых в данный момент нет на складе.
Теперь спросите себя, как бы вы реализовали эту логику. Это может быть сложнее, чем вы думаете. И позвольте мне бросить в кривой мяч. Вам понадобится еще одна функция для обновления записей о запасах при поступлении новых запасов. При обновлении запаса истощенного товара метка « Оставшиеся позиции» и кнопка « Добавить в корзину» должны быть немедленно обновлены, чтобы отразить новое состояние запаса.
В зависимости от вашего мастерства программирования, ваше решение может начать походить на спагетти. Теперь давайте представим, что ваш начальник говорит вам разработать API, который позволяет сторонним сайтам продавать товары напрямую со склада. API должен гарантировать, что основной веб-сайт покупок будет синхронизирован с уровнями запасов продуктов. В этот момент вам хочется выдернуть волосы и спросить, почему вам не сказали это реализовать раньше. Вы чувствуете, что вся ваша тяжелая работа ушла впустую, поскольку вам нужно полностью переработать код, чтобы справиться с этим новым требованием.
Вот где библиотека шаблонов управления состоянием может избавить вас от таких головных болей. Это поможет вам организовать код, который обрабатывает ваши входные данные таким образом, что добавление новых требований становится проще.
Предпосылки
Прежде чем мы начнем, я предполагаю, что вы:
Вам также потребуется последняя версия Node.js, которая не старше версии 6.0. На момент написания этой статьи самые последние версии Node.js v10.13.0 (LTS) и npm версии 6.4.1. Если в вашей системе не установлена подходящая версия Node, я рекомендую использовать менеджер версий .
Наконец, у вас должна быть установлена самая последняя версия Vue CLI :
npm install -g @vue/cli
Построить счетчик используя локальное состояние
В этом разделе мы собираемся создать простой счетчик, который локально отслеживает его состояние. Как только мы закончим, я перейду к фундаментальным концепциям Vuex, прежде чем посмотреть, как переписать счетчик, чтобы использовать официальное решение Vue для управления состоянием.
Начало настройки
Давайте сгенерируем новый проект, используя CLI:
vue create vuex-counter
Откроется мастер, который проведет вас через процесс создания проекта. Выберите Вручную выбрать функции и убедитесь, что вы решили установить 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
. Когда вы будете удовлетворены, не забудьте сбросить его обратно на 0, прежде чем мы перейдем к следующему шагу.
Увеличение и уменьшение
Сразу после 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. Если вы знакомы с подобными фреймворками, такими как Redux, вы не найдете здесь ничего удивительного. Если вы ранее не сталкивались с какими-либо основами управления состоянием на основе Flux, обратите особое внимание.
Магазин Vuex
Магазин предоставляет централизованное хранилище для общего состояния в приложениях Vue. Вот как это выглядит в своей основной форме:
// src/store/index.js import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) export default new Vuex.Store({ state: { // put variables and collections here }, mutations: { // put sychronous functions for changing state eg add, edit, delete }, actions: { // put asynchronous functions that can call one or more mutation functions } })
После определения вашего магазина вам нужно добавить его в ваше приложение Vue.js следующим образом:
// src/main.js import store from './store' new Vue({ store, render: h => h(App) }).$mount('#app')
Это сделает внедренный экземпляр хранилища доступным для каждого компонента в нашем приложении, как this.$store
.
Работа с государством
Также называется единым деревом состояний , это просто объект, который содержит все данные интерфейсного приложения. Vuex, как и Redux, работает в одном магазине. Данные приложения организованы в древовидную структуру. Его конструкция довольно проста. Вот пример:
state: { products: [], count: 5, loggedInUser: { name: 'John', role: 'Admin' } }
Здесь у нас есть products
которые мы инициализировали с пустым массивом, и count
, который инициализируется значением 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
Helper
Теперь предположим, что у вас есть несколько состояний, которые вы хотите отобразить в своих представлениях. Объявление длинного списка вычисляемых свойств может быть многословным, поэтому 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
будет обновлен немедленно. Вот как вы можете получить доступ к обработчику получения из компонента:
computed: { depletedProducts() { return this.$store.getters.depletedProducts; } }
The mapGetters
Helper
Вы можете упростить код getters
с помощью помощника 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
обращаются через метод, он всегда запускается и результат не будет кэшироваться.
Для сравнения:
// property notation, result cached store.getters.depletedProducts // method notation, result not cached store.getters.getProductById(5)
Изменение состояния с помощью мутаций
Важным аспектом архитектуры Vuex является то, что компоненты никогда не изменяют состояние напрямую. Это может привести к странным ошибкам и несоответствиям в состоянии приложения.
Вместо этого, способ изменить состояние в хранилище Vuex — это совершить мутацию . Для тех из вас, кто знаком с Redux, они похожи на редукторы .
Вот пример мутации, которая увеличивает переменную 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; } }
// component updateCount() { this.$store.commit('incrementBy', 25); }
В приведенном выше примере мы передаем мутации целое число, на которое он должен увеличить количество. Вы также можете передать объект в качестве параметра. Таким образом, вы можете легко включить несколько полей, не перегружая обработчик мутаций:
// store.js mutations: { incrementBy(state, payload) { state.count += payload.amount; } }
// component updateCount() { this.$store.commit('incrementBy', { amount: 25 }); }
Вы также можете выполнить коммит в стиле объекта, который выглядит следующим образом:
store.commit({ type: 'incrementBy', amount: 25 })
Обработчик мутаций останется прежним.
mapMutations
Helper
Подобно mapState
и mapGetters
, вы также можете использовать помощник mapMutations
чтобы уменьшить шаблон для ваших обработчиков мутаций:
import { mapMutations } from 'vuex' export default{ methods: { ...mapMutations([ 'increment', // maps to this.increment() 'incrementBy' // maps to 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 with payload store.dispatch('incrementBy', { amount: 25}) // dispatch with object store.dispatch({ type: 'incrementBy', amount: 25 })
Вы можете отправить действие в компонент так:
this.$store.dispatch('increment')
Карта mapActions
Помощник
Кроме того, вы можете использовать помощник mapActions
для назначения обработчиков действий локальным методам:
import { mapActions } from 'vuex' export default { //.. methods: { ...mapActions([ 'incrementBy', // maps this.increment(amount) to this.$store.dispatch(increment) 'incrementAsync', // maps this.incrementAsync() to this.$store.dispatch(incrementAsync) add: 'increment' // maps this.add() to this.$store.dispatch(increment) ]) } }
Перестройте приложение Counter с помощью Vuex
Теперь, когда мы взглянули на основные концепции Vuex, пришло время реализовать то, что мы узнали, и переписать наш счетчик, чтобы использовать официальное решение Vue для управления состоянием.
Если вы представляете себе проблему, вы можете попробовать сделать это самостоятельно, прежде чем читать дальше …
Когда мы сгенерировали наш проект с использованием Vue CLI
, мы выбрали Vuex
качестве одной из функций. Пара вещей произошла:
-
Vuex
был установлен как пакетная зависимость. Проверьте вашpackage.json
чтобы подтвердить это. - Файл
store.js
был создан и внедрен в ваше приложениеmain.js
черезmain.js
Чтобы преобразовать наше приложение счетчика «локальное состояние» в приложение 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, какую проблему он решает, как его установить, а также его основные концепции. Затем мы применили эти концепции для рефакторинга нашего приложения-счетчика для работы с Vuex. Надеемся, что это введение поможет вам в реализации Vuex в ваших собственных проектах.
Следует также отметить, что использование Vuex в таком простом приложении является полным перебором. Тем не менее, в другой статье — Создание приложения списка покупок с помощью Vue, Vuex и Bootstrap Vue — я создам более сложное приложение для демонстрации более реального сценария, а также некоторые более продвинутые функции Vuex.