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-конфиг, чтобы обеспечить автоматизированный деплой нашего приложения после пуша в ветку мастер.