Работа с Vuex в Nuxt
В одной своей статье я уже рассказывал о лучших практиках работы с Vuex в больших приложениях. А в этой статье я покажу вам, как начать работу с Vuex в приложении Nuxt. Как создать свой первый Vuex модуль и как вы можете управлять состоянием в ваших компонентах и страницах приложения Nuxt.js. Я не буду вдаваться в подробности о том, как работает хранилище состояния. Я сосредоточусь на том, как использовать Vuex в приложении Nuxt и покажу как легко начать с ним работать.
Что такое Vuex
Самое простое объяснение заключается в том, что Vuex - это инструмент управления состоянием для Vue. Он хранит глобальное состояние, которое может быть доступно и изменено в вашем приложении. Он позволяет вам работать с состоянием очевидным образом и использовать такие соглашения, как экшены и мутации, для обработки изменений состояния.
Это отдельный модуль в экосистеме Vue, поддерживаемый разработчиками ядра Vue и сообществом.
Вам не обязательно использовать Vuex в приложении Vue, но как только вам понадобится какое-то глобальное состояние или централизованное место для хранения данных, рекомендуется использовать Vuex. Если вы хотите узнать больше о Vuex в деталях, я рекомендую вам почитать документацию Vuex для дальнейшего углубления по этой теме.
Как установить Vuex в приложение Nuxt
Vuex поставляется вместе с Nuxt по умолчанию. При создании нового проекта вы могли заметить пустую папку store. Это место, где мы работаем с Vuex. По умолчанию он отключен, но будет активирован, как только в этой папке будет создан .js
файл. Каждый файл в этой папке будет преобразован в модуль Vuex. Файл index.js
будет корневым модулем.
Вот четыре основных блока, из которых состоит каждый модуль Vuex:
// store/index.js
export const state = () => {}
export const mutations = {}
export const actions = {}
export const getters = {}
Допустим, нам нужен определенный модуль Vuex для учёта фруктов в нашем приложении. Да, давайте работать с фруктами. Почему бы и нет. Это отличная метафора.
Создайте новый .js
файл в папке store
под названием store/fruits.js
и добавьте в него несколько методов.
// store/fruits.js
export const state = () => ({
fruits: [],
})
export const mutations = {
addFruit(state, fruit) {
state.fruits.push(fruit)
},
removeFruit(state, fruitId) {
state.fruits = state.fruits.filter((fruit) => fruit.id !== fruitId)
},
}
export const actions = {
addFruit(context, fruit) {
const slicedFruit = sliceFruit(fruit)
context.commit(slicedFruit)
},
}
export const getters = {
getApples: (state) => {
return state.fruits.filter((fruit) => fruit.type === 'Apple')
},
}
Здесь мы используем и мутацию, и экшен для добавления нового фрукта. Экшены можно использовать, когда вы хотите изменить входящие данные или выполнить асинхронное действие перед комитом данных в хранилище. В данном примере мы вызываем метод, который нарежет фрукт перед добавлением (функция sliceFruit
).
На мой взгляд, если есть экшен, который только передаёт данные мутации, этот экшен можно удалить и использовать мутацию напрямую. Нет смысла в бесполезной прослойке.
Мы также создали геттер, который будет фильтровать список фруктов в хранилище, чтобы вернуть только список яблок. Это полезно, поскольку геттер будет реактивным. Поэтому, когда список фруктов будет обновляться, геттер будет реактивно отражать эти изменения.
Итак, как мы можем использовать созданный модуль в нашем приложении?
Работа с состоянием Vuex из страниц (pages) и компонентов
Использование хелпера контекстного хранилища Nuxt
Доступ к состоянию
Nuxt добавляет свойство store
к объекту контекста, к которому можно получить доступ с помощью вызова this.$store
в любом компоненте Vue. Таким образом, если мы хотим вывести список фруктов, мы можем сделать что-то вроде этого:
<template>
<ul>
<li v-for="fruit in fruits" :key="fruit.id">
{{ fruit.name }} - {{ fruit.type }}
</li>
</ul>
</template>
<script>
export default {
computed: {
fruits() {
return this.$store.fruits.state.fruits
},
},
}
</script>
Обратите внимание, что нам нужно обращаться по имени модуля (по названию файла), чтобы извлечь состояние модуля из $store
. Если бы мы добавили наше состояние в индексный файл, мы могли бы получить доступ к нему непосредственно по свойству $store.state.fruits
. А в нашем случае мы ещё используем вычисляемое (computed) свойство, чтобы получить реактивный список яблок.
Использование экшенов и мутаций
Итак, как мы используем экшены и мутации для добавления нового фрукта? Для демонстрации я жестко захардкодил данные нового фрукта.
<script>
export default {
methods: {
addFruitFromComponent() {
const newFruit = {
name: 'Pink Lady',
type: 'Apple',
id: 2,
}
// Используем dispatch чтобы вызвать экшен
this.$store.dispatch('fruits/addFruit', newFruit)
// Используем коммит чтобы вызвать мутацию
this.$store.commit('fruits/addFruit', newFruit)
},
},
}
</script>
Экшены и мутации можно вызывать непосредственно из свойства $store
. Существует два различных метода: dispatch
и commit
.
Dispatch запускает экшен, а commit вызывается для мутаций данных.
В приведенном выше примере оба метода добавят новое значение в список фруктов, но экшен сначала вызовет метод sliceFruit(...)
. Если вам нужно манипулировать, изменять или проверять добавляемые данные, это следует делать в экшене, а не в методе мутации. А может быть, и вообще перед отправкой в Store, но это уже другая тема.
Важно помнить, что экшены могут обрабатывать асинхронные вызовы. В то время как мутации являются синхронными и должны использоваться только для обновления состояния.
Использование геттеров (getters)
Геттеры работают очень похоже на вычисляемые свойства. С той лишь разницей, что они доступны глобально из любого места приложения. В нашем случае геттер getApples
будет выглядеть следующим образом:
<template>
<ul>
<li v-for="apple in apples" :key="apple.id">
{{ apple.name }}
</li>
</ul>
</template>
<script>
export default {
computed: {
apples() {
return this.$store.fruits.getters.getApples()
},
},
}
</script>
Использование хелперов Vuex (mappers)
Vuex также предоставляет мапперы, которые вы можете использовать вместо вызова this.$store
напрмямую. Для более подробного объяснения того, как их использовать, я снова обращаюсь к документации Vuex, а особенно, к разделу о хелперах биндинга.
Если взять тот же пример, что и выше, но с использованием хелперов биндинга, то код будет выглядеть следующим образом:
<template>
<ul>
<li v-for="apple in apples" :key="apple.id">
{{ apple.name }}
</li>
</ul>
</template>
<script>
import { mapState } from "vuex";
export default {
computed: {
...mapState('fruits', ['fruits']
}
}
</script>
А если ещё применить экшены и мутаций:
<script>
import { mapActions, mapMutations } from 'vuex'
export default {
methods: {
...mapMutations('fruits', ['removeFruit']),
...mapActions('fruits', ['addFruit']),
handleFruits() {
const newFruit = {
name: 'Pink Lady',
type: 'Apple',
id: 2,
}
// Вызов экшена добавления фрукта
this.addFruit(newFruit)
// Вызов мутации удаления фрукта
this.removeFruit(newFruit.id)
},
},
}
</script>
Одним из преимуществ использования хелперов биндинга является то, что вы можете вызывать методы с меньшим количеством кода. Это особенно полезно при вызове экшена или мутации непосредственно в шаблоне компонента (template
).
<!-- С хелпером биндинга -->
<template>
<button @click="addFruit(fruit)" />
</template>
<!-- С хелпером $store-->
<template>
<button @click="$store.addFruit(fruit)" />
</template>
Одним из недостатков является то, что может потеряться визуальная связь с хранилищем состояни Store. Не очевидно, метод this.addFruit
относится к текущему компоненту или к экшену Vuex. Попробуйте эти подходы и решите, что вам больше подходит. В больших проектах вы, скорее всего, столкнетесь с обоими способами. Это может быть опасно, и я рекомендую всегда стремиться к однообразному стилю. Выясните, как вы или ваша команда предпочитает делать подобные вещи, и применяйте это во всех местах в едином стиле.
Использование метода nuxtServerInit
Nuxt предлагает специальный экшен, который позволяет отправить запрос на бекенд (до старта всего приложения), получить какие-то данные (с помощью axios, например), и их передать в начальное состояние приложения Vuex. А затем, это состояние синхронизируется с клиентом. Для этого требуется, чтобы Nuxt работал в универсальном режиме (universal
) и чтобы экшен nuxtServerInit
был определён в файле index.js
.
Пример взят непосредственно из документации экшена nuxtServerInit.
// store/index.js
export const actions = {
nuxtServerInit({ commit }, { req }) {
if (req.session.user) {
commit('user', req.session.user)
}
},
}
Может показаться, что nuxtServerInit
не работает в остальных модулях Vuex. И так оно и есть - этот экшен вызывается только если его добавить в store/index.js
. В других же дочерних модулях он НЕ срабатывает. И это логично, ведь модулей может быть 100500 штук, и каждому может потребоваться запрос на бекенд, что сильно затормозит старт приложения. А нахождение всей логики в index.js
файле позволяет держать под контролем всю комплексность логики инициализации состояния приложения и видеть все начальные запросы.
Но, если вдуг вам понадобится для разных модулей получать состояние на бекенде и задавать его до старта приложения, выход есть :) Вы можете делать нужные запросы в nuxtServerInit
(store/index.js), а потом поочередно вызывать мутации каждого необходимого модуля:
Например, как в этом примере из моего реального проекта:
// store/index.js
export const state = () => ({
usdRate: 0.0,
})
export const mutations = {
setUsdRate(state, usdRate) {
state.usdRate = usdRate
}
}
export const actions = {
async nuxtServerInit ({ commit }) {
const initParams = await this.$configService.getInitParams();
commit('setUsdRate', initParams.usdRate)
commit('user/setAuth', initParams.user)
commit('user/setSubscriptions', initParams.subscriptions)
const favorites = initParams.user.favorites
commit('favorite/setFavoriteList', favorites)
}
}
Заключение
Работа с Vuex в приложении Nuxt проста и удобна. Vuex обеспечивает создание модулей на основе структуры папок и файлов в папке store
. Вспомогательное свойство контекста $store
полезно при обращении к хранилищу состояния из компонентов. Кроме этого, в Nuxt нет никаких специфических способов работы с Vuex по сравнению с обычным приложением Vue. Nuxt позволяет быстро запустить приложение, чтобы вы могли сосредоточиться на реализации, а не на настройке.
Надеюсь что эта статья была вам полезна и вы узнали, как работать с Vuex в приложении Nuxt. Думаю, вы подчерпнули интересные практики из этой статьи. Уверен, что теперь у вас не возникает вопрос, как делать запросы к бекенду до загрузки страницы и синхронизировать состояние бекенда и Nuxt приложения.