SPA-приложение на Vue.JS #5
Привет! В прошлом видео мы научились получать данные асинхронно с помощью Axios, а сегодня мы эти данные будем сохранять во Vuex.
Что такое Vuex?
Если говорить в общем - это state-manager, модуль для управления состоянием приложения. Пример из нативного javascript: есть такая часть объекта window как localStorage, в нём можно сохранять любые пользовательские данные, которые в любой момент оттуда можно получить. Использование localStorage позволяет хранить данные (а точнее, их часть) на клиенте, избегая постоянных обращений к серверу. Vuex несколько шире простого хранилища данных: мы имеем определённые методы для всех объектов, в которых хранятся данные и можем при изменении этих данных поменять состояние нашего приложения с помощью мутаций. Vuex позволяет отделить управление состоянием приложения от самих его компонентов.
То есть, говоря более конкретно: Vuex нам нужен для того, чтобы растащить по разным модулям дерево компонентов Vue и общее состояние приложения. При этом, у нас появляется доступ ко всем данным внутри конкретного состояния из любой компоненты.
Подключение
В папке src создаём папку store. В ней создаём файл index.js со следующим содержимым:
import Vue from 'vue' import Vuex from 'vuex' // import your store modules here Vue.use(Vuex) export default new Vuex.Store({ modules: { // put your store modules } })
В файлике src/main.js импортируем store:
import store from '@/store'
Не забываем передать его и в элемент класса Vue (как router и vuetify).
Пишем свои store-модули
Для начала создадим общий модуль (файл common.js). Он и будет отвечать за общее состояние нашего приложения.
export default { state: { loading: false, error: null }, mutations: { setLoading (state, payload) { state.loading = payload }, setError (state, payload) { state.error = payload }, clearError (state) { state.error = null } }, actions: { setLoading ({ commit }, payload) { commit('setLoading', payload) }, setError ({ commit }, payload) { commit('setError', payload) }, clearError ({ commit }, payload) { commit('clearError') } }, getters: { loading (state) { return state.loading }, error (state) { return state.error } } }
Разберём структуру по порядку:
state
— содержит в себе состояние данного объекта store. В формате ключ: значение
.
mutations
— это мутации, они должны менять состояние. Мутации синхронны, поэтому получать данные асинхронным образом внутри них — не получится. Поэтому инициировать мутации следует только из actions
.
actions
— объект, который содержит в себе все действия, которые можно производить с данными: получать их асинхронно, обрабатывать и инициировать мутации, чтобы записать данные в состояние.
getters
— нужны для получения состояния.
Импортируйте его в store, туда, где указано комментариями.
Теперь перейдём к работе с модулями. Чтобы использование Vuex было полностью оправдано — мы будем получать теперь не только общую информацию, но и топ-5 стран, которые будем выводить вот в этом меню:
Для начала создадим файлик countryPrototype, который будет возвращать конструктор для каждого объекта country, сохраняемого в store.
export default class Country { constructor ( id, name, cases, deaths, recoveries, visualData, visualLabels ) { this.id = id this.name = name this.cases = cases this.deaths = deaths this.recoveries = recoveries this.visualData = visualData this.visualLabels = visualLabels } }
Это нужно для того, чтобы автоматизировать процесс создания нового объекта внутри нашего store и избежать ошибок, поскольку объект будет создаваться "по образу и подобию", что называется.
Чтобы получить топ-5 стран: https://corona.lmao.ninja/v2/countries?sort=active
import Country from './countryPrototype' import axios from 'axios' export default { state: { currCountryName: null, countries: [], topCountries: [] }, mutations: { newCountry (state, payload) { state.countries.push(payload) }, newCountryName (state, payload) { state.currCountryName = payload }, newTopCountry (state, payload) { state.topCountries = payload } }, actions: { newCountry ({ commit, getters }, payload) { commit('clearError') commit('setLoading', true) console.log('new country is', payload) axios .get(`https://corona.lmao.ninja/v2/historical/${payload}?lastdays=30`) .then((response) => { let data = response.data let labels = [] let datasets = {} let cases = [] let deaths = [] let recoveries = [] for (let key in data.timeline.cases) { labels.push(key) cases.push(data.timeline.cases[key]) deaths.push(data.timeline.deaths[key]) recoveries.push(data.timeline.recovered[key]) } datasets['cases'] = cases datasets['deaths'] = deaths datasets['recoveries'] = recoveries let id = getters.countryId const сountry = new Country( id, data.country, cases[cases.length - 1], deaths[deaths.length - 1], recoveries[recoveries.length - 1], datasets, labels ) console.log('new state is', this.state) commit('setLoading', false) commit('newCountry', сountry) }) .catch((error) => { commit('setLoading', false) commit('setError', error.message) throw error }) }, newCountryName ({ commit }, payload) { commit('newCountryName', payload) }, updateTopCountries ({ commit, dispatch }) { commit('clearError') commit('setLoading', true) axios .get('https://corona.lmao.ninja/v2/countries?sort=active') .then((response) => { let data = response.data.slice(0, 5) data = data.map((country) => { return country.country }) commit('newTopCountry', data) for (let country of data) { dispatch('newCountry', country) } commit('setLoading', false) }) } }, getters: { country (state) { console.log('data is', state.countries.find(country => country.name === state.currCountryName)) return state.countries.find(country => country.name === state.currCountryName) }, currentCountry (state) { console.log('currentCountry', state.currCountryName) return state.currCountryName }, countryId (state) { return state.countries.length }, topCountries (state) { console.log('countries:', state.countries) return state.topCountries } } }
Компоненты + store
Получение данных: this.$store.getters.key
, где key — ключик, в который Вы записали нужный Вам объект.
Запись данных в store (а точнее — вызов действия, который эту запись будет производить): this.$store.dispatch('action', arguments)
, где action
— имя вашего действия, а arguments
— аргументы, которые будут в него переданы.
В компоненте App.vue надо добавить вычисляемое свойство:
computed: { countries () { return this.$store.getters.topCountries } }
Вычисляемые свойства нужны для того, чтобы результат их работы кэшировался и обновлялся автоматически только в том случае, если в цепочке есть изменения.
Меняем содержимое нашего <v-list>
, который открывается по нажатию на три bullet-points:
<v-list-item v-for="country in countries" :key="country" @click="() => {}" link :to="'/country/' + country"> <v-list-item-title @click="updateCountry(country)">{{ country }}</v-list-item-title> </v-list-item>
countries
здесь — то самое вычисляемое свойство, не забудьте указать атрибут link
и :to
. Метод updateCountry() очень прост:
methods: { updateCountry (country) { this.$store.dispatch('newCountryName', country) } }
И не забудьте, что ровно в тот момент, когда наша компонента App.vue будет смонтирована — надо обновить список топ-5 стран:
mounted () { this.$store.dispatch('updateTopCountries') }
Теперь Country.vue. По логике она такая же, как и Total.vue, но только карточки и графики теперь — вычисляемые свойства:
computed: { countryDataCards () { let countryData = this.$store.getters.country let cards = [] cards.push({ title: 'total cases', bgColor: 'primary lighten-2', amount: countryData.cases, amountNew: 0, icon: 'mdi-alert-box' }) cards.push({ title: 'deaths', bgColor: 'red accent-2', amount: countryData.deaths, amountNew: 0, icon: 'mdi-emoticon-dead' }) cards.push({ title: 'recoveries', bgColor: 'teal lighten-1', amount: countryData.recoveries, amountNew: 0, icon: 'mdi-hospital-box' }) return cards }, countryDataVisuals () { let countryData = this.$store.getters.country let visuals = [] visuals.push({ id: 1, chartData: { labels: countryData.visualLabels, datasets: [{ label: 'Total cases', backgroundColor: '#6aaaff', data: countryData.visualData.cases }] }, options: { responsive: true, maintainAspectRatio: false } }) visuals.push({ id: 2, chartData: { labels: countryData.visualLabels, datasets: [{ label: 'Deaths', backgroundColor: '#ff5252', data: countryData.visualData.deaths }] }, options: { responsive: true, maintainAspectRatio: false } }) visuals.push({ id: 3, chartData: { labels: countryData.visualLabels, datasets: [{ label: 'Recoveries', backgroundColor: '#26a69a', data: countryData.visualData.recoveries }] }, options: { responsive: true, maintainAspectRatio: false } }) return visuals } }
Соответственно, вместо карточек (cards
) и графиков (visuals
) используйте теперь имена вычисляемых свойств (countryDataCards
и countryDataVisuals
).
Не забудьте и про передачу пропса queryName из параметров роутера
export default { name: 'Country', props: ['queryName'], <...>
import Vue from 'vue' import Router from 'vue-router' import Total from '@/components/Total' import Countries from '@/components/Countries' import Country from '@/components/Country' import store from '@/store' Vue.use(Router) export default new Router({ routes: [ { path: '/', name: 'Total', component: Total }, { path: '/countries', name: 'Countries', component: Countries }, { path: '/country/:queryName', name: 'Country', component: Country, props: true, beforeEnter (to, from, next) { let queryName = to.params.queryName store.dispatch('newCountryName', queryName) next() } } ] })
Исходники на гитхабе, я на всякий случай, обновил.
Заключение
На этом сегодня всё. В следующем видео мы разберём CI/CD и напишем простой NGINX-конфиг, чтобы обеспечить автоматизированный деплой нашего приложения после пуша в ветку мастер.