/ Nuxt

Работа с 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 приложения.