Пошаговое руководство, раскрывающее сложности, возникающие без государственного управления.

Обзор

Эта статья представляет собой учебник по управлению состоянием применительно к приложениям Vue. Мы рассмотрим реализацию базового примера корзины покупок без глобального управления состоянием; это должно показать, в чем заключаются сложности, которые могут затруднить долгосрочную ремонтопригодность. Мы рассмотрим анатомию Vuex и реализуем базовую настройку. Наконец, мы проведем рефакторинг нашей реализации корзины покупок, чтобы использовать Vuex.

Ресурсы и настройка

Расширение Vue.js Devtools для Chrome

Если вы еще этого не сделали, обязательно скачайте Chrome-расширение Vue.js devtools. Он обеспечивает необходимую видимость любого проекта Vue; сегодня мы будем использовать его, чтобы наблюдать за нашими событиями и состоянием vuex.



Github Repo

Https://github.com/KevinBigelow/state-mgmt-w-vuex/tree/startingPoint

Клонируйте или загрузите ветку startPoint из репозитория выше. Эта ветка содержит некоторые очень простые функции для добавления и удаления товаров из корзины пользователя.

Mockapi.io

Мы используем mockapi.io для репликации базового API. Я создал проект с конечными точками корзины и товаров; вам придется его клонировать.

  1. Зарегистрируйте бесплатную учетную запись здесь.
  2. После входа в систему клонируйте мой проект, перейдя по следующей ссылке https://mockapi.io/clone/5f21f583daa42f0016666158

Аксиос

Мы используем библиотеку Axios для обработки наших вызовов API. Нам нужно будет установить baseUrl на конечную точку вашего проекта mockapi.

  1. Скопируйте конечную точку api из вашего проекта mockapi

2. Откройте src / axios / index.js и вставьте URL-адрес конечной точки в качестве значения для baseUrl.

import Axios from 'axios'
const axios = Axios.create({
    baseUrl: 'https://YOUR-ENDPOINT-ID.mockapi.io'
});
export default axios

Запустить проект

Откройте окно терминала, перейдите туда, где вы клонировали свой проект, запустите сервер разработки с помощью следующей команды

npm run serve

Затем вы сможете посетить http: // localhost: 8080 /, чтобы увидеть, как работает проект. Вы должны увидеть список продуктов. Нажмите на один, чтобы просмотреть страницу продукта. Кнопка Добавить в корзину на этой странице делает именно то, что вы думаете. Просмотрите свою корзину, нажав кнопку Корзина в заголовке.

Большой! Вы настроены и готовы начать.

Часть 1: Без государственного управления.

Прежде чем перейти к Vuex, давайте посмотрим, как решить простую проблему без какого-либо управления состоянием.

Вы, наверное, заметили, что нажатие кнопки «Добавить в корзину» не влияет на счетчик на кнопке корзины в заголовке. Будьте уверены, товар был добавлен в корзину, вы можете проверить страницу корзины, чтобы подтвердить. Давайте посмотрим, что нужно для увеличения счетчика при добавлении товара в корзину.

CartButton.vue - это компонент, которому потребуются обновленные данные.

<template>
  <router-link :to="{name: 'Cart'}" class="button is-primary">
    Cart ({{count}})
  </router-link>
</template>

<script>
export default {
  props: {
    count: Number
  }
}
</script>

Посмотрите, как он получает счетчик как опору, а затем печатает счетчик в шаблоне с синтаксисом фигурных скобок {{count}}

Вы найдете CartButton.vue включенным в App.vue, который связывает вычисленное свойство cartLength с count опорой.

<cart-btn :count="cartLength" class="level-right"></cart-btn>

На созданном крючке жизненного цикла (все еще в App.vue) мы вызываем корзину с помощью Axios. Затем мы обновляем this.cart ответ. Вычисленное свойство cartLength возвращает длину this.cart.

<script>
import cartBtn from '@/components/CartButton.vue'
import axios from '@/axios'

export default {
    data () {
        return {
            cart: []
        }
    },
    computed: {
        cartLength () {
            return this.cart.length
        }
    },
    components: {
        'cart-btn': cartBtn
    },
    methods: {
        async getCart() {
            await axios.get(`cart`).then(response => {
                this.cart = response.data
            })
        }
    },
    created() {
        this.getCart()
    }
}
</script>

Чтобы сообщить об изменении в корзине, нам нужно будет передать событие от компонента AddToCart.vue на всем пути к компоненту CartButton.vue. Поскольку дочерние компоненты могут отправлять события только своему непосредственному родительскому элементу, каждый дочерний компонент между AddToCart.vue и CartButton.vue должен будет генерировать событие. См. Схему ниже.

Чтобы сгенерировать первое событие, откройте компонент AddToCart.vue и обновите метод addToCart(). Имеет смысл запускать событие после успешной публикации в api.

addToCart () {
    axios.post('cart', this.product).then(() => {
        this.$emit('add-product-to-cart')
    })
},

Теперь перейдем к компоненту Product.vue, в который включен компонент AddToCart.vue. Найдите пункт where<add-to-cart>, включенный в шаблон, и обновите его до следующего.

<add-to-cart :product="product" v-on:add-product-to-cart="$emit('add-product-to-cart')"></add-to-cart>

Вот как это работает:

v-on ожидает событий от экземпляра этого компонента, в данном случае мы ожидаем add-product-to-cart. Поскольку у нас есть еще один уровень компонента, который необходимо пройти, прежде чем мы получим доступ к компоненту CartButton.vue, необходимо создать еще одно событие. Для простоты мы дадим ему то же имя $emit(‘add-product-to-cart’)

Перейдите на страницу продукта и откройте вкладку событий на панели Vue devtools. Когда вы нажимаете кнопку Добавить в корзину, вы должны увидеть 2 события.

См. Свойство payload в информации о событии? Мы можем передавать данные с событиями. А поскольку cartLength вычисляет количество элементов в this.cart, мы хотим добавить продукт в this.cart в App.vue. Для этого добавим продукт в полезную нагрузку в компоненте Product.vue.

<add-to-cart :product="product" v-on:add-product-to-cart="$emit('add-product-to-cart', product)"></add-to-cart>

Теперь обновите страницу продукта и снова нажмите кнопку Добавить в корзину. Вы должны увидеть продукт, возвращенный в качестве полезной нагрузки в случае, когда это было инициировано компонентом Product.vue.

Отлично, давайте перейдем к App.vue; мы добавим слушателя в <router-view/> - добавление слушателей в представление роутера не идеально, но в нашем базовом примере это сработает.

<router-view v-on:add-product-to-cart="onAddProductToCart"/>

На этот раз мы собираемся вызвать метод onAddProductToCart, который добавит продукт из полезной нагрузки в this.cart - давайте добавим этот метод в App.vue.

onAddProductToCart (product) {
    this.cart.unshift(product)
}

Отлично, теперь посмотрим, обновляется ли наш счетчик.

Откройте страницу продукта и снова нажмите кнопку Добавить в корзину; вы должны увидеть увеличение счета на 1.

Вы также можете открыть вкладку компонентов в расширении Vue devtools, чтобы наблюдать за ростом массива тележек по мере добавления продуктов. Теперь мы знаем, что количество правильно отражает размер тележки.

Оно работает!

Да, это работает. Но видите ли вы, где все усложняется в приложении производственного уровня?

Взгляните на страницу корзины. Когда вы нажимаете Удалить из корзины, ничего не меняется. Аналогичная проблема требует передачи события с идентификатором элемента из компонента RemoveFromCart.vue в компонент Cart.vue. Оттуда вы должны отфильтровать корзину, чтобы исключить элемент с идентификатором, равным указанному вами идентификатору.

Попробуйте сами, или скачайте ветку propsWithEvents из репозитория, или просто взгляните на разницу фиксации.

Часть 2: Настройка управления состоянием с помощью Vuex

Мы будем проводить рефакторинг кода, который мы написали до этого момента.

Управление состоянием построено на концепции наличия единого источника достоверной информации в приложении. Когда несколько компонентов зависят от одного и того же набора данных, нет смысла управлять данными так, как мы делали в первой части этой статьи.

Когда дело доходит до управления состоянием, есть несколько вариантов, мы рассмотрим их через призму Vuex. Концепция остается неизменной независимо от того, какую технологию вы используете.

Сначала вам нужно установить Vuex. Откройте окно терминала и перейдите в каталог своего проекта, чтобы выполнить следующее.

npm install vuex

Теперь создайте новый каталог с именем store в своем проекте в src /

Затем создайте файл index.js в каталоге store.

Чтобы создать магазин Vuex, добавьте следующее в свой новый файл index.js.

import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex);

export default new Vuex.Store({
    state: {},
    getters: {},
    actions: {},
    mutations: {}
})

Последний шаг - включить этот магазин в ваш экземпляр Vue. Откройте файл main.js, import store from ‘@/store’ и добавьте store к экземпляру.

import Vue from 'vue'
import App from './App.vue'
import store from '@/store'
import router from './router'

Vue.config.productionTip = false

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')

Большой! Теперь ваш проект настроен на Vuex.

Базовый обзор анатомии Vuex Store

Состояние: здесь мы храним наши данные. Например, когда API возвращает список продуктов, мы сохраняем их здесь.

Геттеры: мы используем геттеры в наших компонентах при доступе к состоянию. На мой взгляд, именно в доступе в стиле метода геттеры действительно проявляют свою силу.

Мутации. Мутации используются для обновления данных в нашем состоянии. Как при добавлении товара в корзину.

Действия. Мы используем действия для асинхронных операций, таких как вызов API. Действия почти всегда совершают мутации. Когда api для продуктов отвечает, мы фиксируем мутацию, которая устанавливает state.products для ответа.

Настройка состояния и геттеров

Начнем с добавления массива тележек в наше состояние.

import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex);

export default new Vuex.Store({
    state: {
        cart: []
    },
    getters: {},
    actions: {},
    mutations: {}
})

Мы получим доступ к state.cart в наших компонентах Cart.vue и CartButton.vue с помощью геттера. Теперь добавим геттер.

import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex);

export default new Vuex.Store({
    state: {
        cart: []
    },
    getters: {
        getCart: state => state.cart
    },
    actions: {},
    mutations: {}
})

Мы также можем использовать геттер, чтобы получить длину тележки, давайте это тоже добавим. В этом случае мы можем использовать геттер getCart. См. Документацию Доступ к стилю свойств

import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex);

export default new Vuex.Store({
    state: {
        cart: []
    },
    getters: {
        getCart: state => state.cart,
        getCartLength: (state, getters) => {
            return getters.getCart.length
        }
    },
    actions: {},
    mutations: {}
})

Теперь давайте воспользуемся этим getCartLength получателем в нашем компоненте CartButton.vue.

Вместо того, чтобы передавать счетчик в качестве опоры, мы будем использовать вычисляемое свойство для использования геттера getCartLength. Откройте CartButton.vue и обновите его до следующего.

<template>
  <router-link :to="{name: 'Cart'}" class="button is-primary">
    Cart ({{count}})
  </router-link>
</template>

<script>
import { mapGetters } from 'vuex'

export default {
  computed: {
    ...mapGetters({
      count: 'getCartLength'
    })
  }
}
</script>

Мы используем помощник mapGetters, есть и другие способы доступа к вашим геттерам, вы можете прочитать о них здесь.

Обратите внимание, что мы сопоставляем ‘getCartLength’ с count, чего и ожидает наш шаблон.

Откройте резервную копию вашего проекта в браузере. Кнопка корзины должна указывать на то, что товары не были добавлены (даже если вы добавляли товары). Это потому, что мы еще не обновили свое состояние.

Настройка действий и мутаций

Взгляните на метод getCart() в App.vue.

async getCart() {
    await axios.get(`cart`).then(response => {
        this.cart = response.data
    })
},

Мы собираемся сделать что-то очень похожее с нашим действием, но вместо this.cart = response.data мы запустим мутацию, которая устанавливает state.cart в response.data

Удалите все, кроме следующего, из тега скрипта в App.vue.

<script>
import cartBtn from '@/components/CartButton.vue'

export default {
    components: {
        'cart-btn': cartBtn
    },
    methods: {
        onAddProductToCart (product) {
            this.cart.unshift(product)
        }
    }
}
</script>

Вам также следует удалить :count привязку данных из cart-btn в шаблоне App.vue. Это должно выглядеть так <cart-btn class=”level-right”></cart-btn>

Откройте store / index.js и добавьте следующее действие и мутацию.

import Vue from 'vue'
import Vuex from 'vuex'
import axios from '@/axios'
Vue.use(Vuex);

export default new Vuex.Store({
    state: {
        cart: []
    },
    getters: {
        getCart: state => state.cart,
        getCartLength: state => state.cart.length
    },
    actions: {
        async fetchCart() {
            await axios.get(`cart`)
        },
    },
    mutations: {
        setCart: (state, cart) => (state.cart = cart)
    }
})

Это действие приведет к загрузке корзины, а мутация установит state.cart как все, что было передано. Давайте свяжем их вместе, зафиксировав мутацию setCart после того, как наш запрос получения будет успешным.

export default new Vuex.Store({
    state: {
        cart: []
    },
    getters: {
        getCart: state => state.cart,
        getCartLength: state => state.cart.length
    },
    actions: {
        async fetchCart({ commit }) {
            await axios.get(`cart`).then(response => {
                commit('setCart', response.data)
            })
        },
    },
    mutations: {
        setCart: (state, cart) => (state.cart = cart)
    }
})

Теперь нам нужно отправить действие fetchCart. Давайте разберемся с этим в созданном хуке жизненного цикла в CartButton.vue. Поэтому после создания экземпляра компонента CartButton.vue будет вызываться fetchCart.

<template>
  <router-link :to="{name: 'Cart'}" class="button is-primary">
    Cart ({{count}})
  </router-link>
</template>

<script>
import { mapGetters, mapActions } from 'vuex'

export default {
  computed: {
    ...mapGetters({
      count: 'getCartLength'
    })
  },
  methods: {
    ...mapActions({
      fetchCart: 'fetchCart'
    })
  },
  created () {
    this.fetchCart()
  }
}
</script>

На этом этапе мы загружаем корзину, сохраняем ответ на state.cart и печатаем {{count}} с помощью getCartLength получателя.

Вы должны увидеть, что счетчик корзины отражает количество элементов в корзине при создании компонента CartButton.vue, но счетчик не увеличивается, когда элемент добавляется в корзину. Давайте разберемся с этим дальше.

Добавление продукта в state.cart

В первой части этой статьи мы передали события, чтобы обновить корзину.

С Vuex нам просто нужно запустить мутацию после того, как продукт будет успешно добавлен в корзину.

Давайте создадим действие addToCart и мутацию addProductToCart.

export default new Vuex.Store({
    state: {
        cart: []
    },
    getters: {
        getCart: state => state.cart,
        getCartLength: state => state.cart.length
    },
    actions: {
        async fetchCart({ commit }) {
            await axios.get(`cart`).then(response => {
                commit('setCart', response.data)
            })
        },
        async addToCart ({ commit }, product) {
            axios.post('cart', product).then(() => {
                commit('addProductToCart', product)
            })
        },
    },
    mutations: {
        setCart: (state, cart) => (state.cart = cart),
        addProductToCart: (state, product) => (state.cart.unshift(product))
    }
})

Теперь давайте удалим старый метод addToCart() из AddToCart.vue, и заменим его нашим действием.

<template>
    <button @click="addToCart(product)" class="button is-primary">Add to Cart</button>
</template>

<script>
import { mapActions } from 'vuex'

export default {
    props: {
        product: {
            type: Object,
            required: true
        }
    },
    methods: {
        ...mapActions({
            addToCart: 'addToCart'
        })
        a̶d̶d̶T̶o̶C̶a̶r̶t̶ ̶(̶)̶ ̶{̶
̶ ̶ ̶ ̶ ̶ ̶ ̶ ̶ ̶ ̶ ̶ ̶ ̶a̶x̶i̶o̶s̶.̶p̶o̶s̶t̶(̶'̶c̶a̶r̶t̶'̶,̶ ̶t̶h̶i̶s̶.̶p̶r̶o̶d̶u̶c̶t̶)̶.̶t̶h̶e̶n̶(̶(̶)̶ ̶=̶>̶ ̶{̶
̶ ̶ ̶ ̶ ̶ ̶ ̶ ̶ ̶ ̶ ̶ ̶ ̶ ̶ ̶ ̶ ̶t̶h̶i̶s̶.̶$̶e̶m̶i̶t̶(̶'̶a̶d̶d̶-̶p̶r̶o̶d̶u̶c̶t̶-̶t̶o̶-̶c̶a̶r̶t̶'̶)̶
̶ ̶ ̶ ̶ ̶ ̶ ̶ ̶ ̶ ̶ ̶ ̶ ̶}̶)̶
̶ ̶ ̶ ̶ ̶ ̶ ̶ ̶ ̶}̶
    }
}
</script>

И мы можем удалить прослушиватели событий в Product.vue / App.vue

<add-to-cart :product="product" ̶v̶-̶o̶n̶:̶a̶d̶d̶-̶p̶r̶o̶d̶u̶c̶t̶-̶t̶o̶-̶c̶a̶r̶t̶=̶"̶$̶e̶m̶i̶t̶(̶'̶a̶d̶d̶-̶p̶r̶o̶d̶u̶c̶t̶-̶t̶o̶-̶c̶a̶r̶t̶'̶,̶ ̶p̶r̶o̶d̶u̶c̶t̶)̶"̶></add-to-cart>
<router-view  ̶v̶-̶o̶n̶:̶a̶d̶d̶-̶p̶r̶o̶d̶u̶c̶t̶-̶t̶o̶-̶c̶a̶r̶t̶=̶"̶o̶n̶A̶d̶d̶P̶r̶o̶d̶u̶c̶t̶T̶o̶C̶a̶r̶t̶"̶/>

Большой! Теперь нажатие кнопки Добавить в корзину обновит state.cart после успешной публикации. Мы также можем увидеть обновление счетчика в компоненте CartButton.vue.

Реализация наших методов получения и действий в компоненте Cart.vue

Вот как теперь должен выглядеть файл Cart.vue.

Мы используем наше fetchCart действие в созданной ловушке жизненного цикла, а затем наш getCart геттер в качестве вычисляемого свойства для распечатки элементов в шаблоне.

<script>
import { mapGetters, mapActions } from 'vuex'
import removeFromCart from '@/components/RemoveFromCart'

export default {
    computed: {
        ...mapGetters({
            cart: 'getCart'
        })
    },
    components: {
        'remove-from-cart': removeFromCart
    },
    methods: {
        ...mapActions({
            fetchCart: 'fetchCart'
        }),
        onRemoveFromCart (productId) {
            this.cart = this.cart.filter(product => product.id !== productId)
        }
    },
    created () {
        this.fetchCart()
    }
}
</script>

Нам все еще нужно использовать наш Vuex при удалении товаров из корзины. Вот оставшиеся задачи:

  1. Преобразуйте метод removeFromCart в компоненте RemoveFromCart.vue в действие.
  2. Преобразование метода onRemoveFromCart в Cart.vue в мутацию
  3. Зафиксируйте onRemoveFromCart мутацию, когда действие removeFromCart будет успешным.

Посмотрите, сможете ли вы сделать это самостоятельно, или загрузите ветку vuex из моего репо, или просмотрите этот коммит, чтобы увидеть, как я это сделал.

Заключение

Надеюсь, это руководство помогло вам понять полезность управления состоянием с помощью Vuex. Если у вас есть вопросы, не стесняйтесь оставлять комментарии. 🖖