Привет, Хабр! представляю вашему вниманию перевод статьи «Mastering Vuex?—?Zero to Hero» автора Sanath Kumar.


Официальная документация Vuex определяет его как паттерн управления состоянием + библиотека для приложений Vue.js. Но что это значит? Что такое паттерн управления состоянием?


Представьте, что вы работаете над большим веб-приложением с сотнями маршрутов и компонентов. Не было бы проще, если бы мы могли хранить все данные, которые нам когда-либо понадобятся в приложении, в одном централизованном хранилище?



Каждый компонент или маршрут внутри нашего приложения будет запрашивать данные из состояния Vuex и передавать измененные данные обратно в состояние.


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


Данные хранятся внутри состояния как JSON-объект. Например:


state: {
  name: "John Doe",
  age: "28"
}

Но как наши компоненты и маршруты могут получить доступ к данным, хранящимся в нашем состоянии? Для этого внутри нашего хранилища Vuex необходимо определить Геттеры, которые будут возвращать данные из хранилища нашим компонентам. Давайте посмотрим, как выглядит простой Геттер, который получает имя из нашего хранилища:


getters: {
  NAME: state => {
    return state.name;
  },
}

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


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


let name = this.$store.getters.NAME;

Мы выяснили, как получить данные из хранилища. Теперь посмотрим, как мы можем установить данные в хранилище. Мы определим сеттеры, верно? Кроме того, Vuex-сеттеры именуются немного иначе. Мы определяем Мутацию (Mutation) для установки данных в наше состояние Vuex.


mutations: {
  SET_NAME: (state, payload) => {
    state.name = payload;
  },
}

Что еще за payload? Payload — это данные, передаваемые нашей мутации из компонента, совершающего мутацию. Как мы можем это осуществить? Очень просто:


this.$store.commit('SET_NAME', your_name);

Этот фрагмент кода изменит состояние приложения и установит любое значение, присвоенное your_name, для свойства name внутри нашего хранилища.


МУТАЦИИ СИНХРОННЫ


Представьте, что у нас есть список имен, хранящихся в базе данных на удаленном сервере. Сервер предоставляет нам эндпоинт, возвращающий массив имен, которые могут быть использованы в нашем приложении Vue.js. Конечно, мы можем использовать Axios, чтобы сделать запрос к эндпоинту и получить данные.


let {data} = await Axios.get('https://myapiendpoint.com/api/names');

После этого мы можем передать возвращенный массив в наш магазин состояние Vuex с помощью мутации. Легко, правда? Но не совсем. Мутации являются синхронными, и мы не можем запускать асинхронные операции, такие как вызовы API, внутри мутации.


Что нам в таком случае делать? Создавать Действия (Actions).


Действия подобны мутациям, но вместо того, чтобы напрямую изменять состояние, они совершают мутацию. Звучит запутанно? Давайте посмотрим на объявление действия.


actions: {
  SET_NAME: (context, payload) {
    context.commit('SET_NAME', payload);
  },
}

Мы определили действие с именем SET_NAME, которое принимает контекст и payload в качестве параметров. Действие совершает мутацию SET_NAME, созданную ранее, с переданными ей данными, то есть your_name.


Теперь вместо того, чтобы вызывать мутацию напрямую, наши компоненты запускают действие SET_NAME с новым именем в качестве данных следующим образом:


this.$store.dispatch('SET_NAME', your_name);

Затем действие инициирует мутацию с переданной ей данными, то есть your_name.



Но почему?


Возможно, вам интересно, почему требуется объявление действия, если мы можем просто инициировать мутации с новым значением непосредственно из наших компонентов. Как упоминалось выше, мутации являются синхронными, а действий — нет.


В приведенном выше примере рассмотрен случай, когда вам нужно обновить значение имени, но не только в своем состоянии, а еще и в базе данных, запущенной на удаленном сервере. Я уверен, что именно так вы собираетесь использовать Vuex в реальном проекте в 99% случаев. Взгляните на следующий фрагмент кода:


mutations: {
  SET_NAME: (state, name) => {
    state.name = name;
  },
},

actions: {
  SET_NAME: async (context, name) => {
    let {data} = await Axios.post('http://myapiendpoint.com/api/name', {name: name});

    if (data.status == 200) {
      context.commit('SET_NAME', name);
    }
  },
}

Сам код не требует пояснений. Мы используем Axios для отправки имени на эндпоинт. Если POST-запрос выполнен успешно, и значение имени поля было успешно изменено на сервере, мы инициируем мутацию SET_ NAME для обновления значения имени внутри нашего состояния.


ВОЗЬМИТЕ ЗА ПРАКТИКУ НИКОГДА НЕ ИНИЦИИРОВАТЬ МУТАЦИИ НАПРЯМУЮ. ДЛЯ ЭТОГО ВСЕГДА ИСПОЛЬЗУЙТЕ ДЕЙСТВИЯ.



Настройка хранилища Vuex во Vue.JS


Давайте погрузимся глубже и узнаем, как мы можем реализовать Vuex в реальном приложении.


Шаг 1. Установка Vuex


npm install --save vuex

Шаг 2. Создание хранилища Vuex


  1. Создайте директорию store в корне нашего приложения.
  2. Создайте файл index.js в этой директории и используйте код, представленный ниже, для создания нового хранилища.

import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

export const store = new Vuex.Store({
  state: {},
  getters: {},
  mutations: {},
  actions: {},
});

Шаг 3. Добавление хранилища Vuex в приложение Vue.JS


1. Импортируйте хранилище в файл main.js:


import {store} from './store';

2. Добавьте хранилище к экземпляру Vue, как показано ниже:


new Vue({
  el: '#app',
  store,
  router,
  render: h => h(App),
});

Теперь мы можем добавить переменные состояния, геттеры, мутации и действия в наше хранилище Vuex.



Пример


Взгляните на хранилище Vuex простого приложения списка дел. “Только не очередной список дел!!!”. Да? Не волнуйтесь. По окончанию данной статьи вы научитесь использовать всю силу и мощь Vuex.


import Vue from 'vue';
import Vuex from 'vuex';
import Axios from 'axios';

Vue.use(Vuex);

export const store = new Vuex.Store({
  state: {
    todos: null,
  },

  getters: {
    TODOS: state => {
      return state.todos;
    },
  },

  mutations: {
    SET_TODO: (state, payload) => {
      state.todos = payload;
    },

    ADD_TODO: (state, payload) => {
      state.todos.push(payload);
    },
  },

  actions: {
    GET_TODO: async (context, payload) => {
      let {data} = await Axios.get('http://yourwebsite.com/api/todo');
      context.commit('SET_TODO', data);
    },

    SAVE_TODO: async (context, payload) => {
      let {data} = await Axios.post('http://yourwebsite.com/api/todo');
      context.commit('ADD_TODO', payload);
    },
  },
});


Добавление нового элемента в список дел


Внутри вашего компонента инициируйте действие SAVE_TODO, передав в него новый элемент списка дел, как показано в фрагменте кода ниже.


let item = 'Get groceries';
this.$store.dispatch('SAVE_TODO', item);

Действие SAVE_TODO делает POST-запрос к эндпоинту, а затем инициирует мутацию ADD_TODO, которая добавляет элемент списка дел в переменную состояния todos.



Получение элементов списка дел


Внутри блока mounted() вашего компонента инициируйте второе действие GET_TODO, которое получает все элементы списка дел из эндпоинта и сохраняет их в переменную состояния todos, инициируя мутацию SET_TODO:


mounted() {
  this.$store.dispatch('GET_TODO');
}


Доступ к элементам списка дел внутри компонента


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


computed: {
  todoList() {
    return this.$store.getters.TODOS;
  },
}

Внутри компонента вы можете получить доступ к вычисляемому свойству:


<div class="todo-item" v-for="item in todoList"></div>


Использование метода mapGetters


Существует еще более простой способ доступа к элементам списка дел внутри компонента с помощью метода mapGetters, предоставляемого Vuex.


import {mapGetters} from 'vuex;

computed : {
  ...mapGetters(['TODOS']),
  // Другие вычисляемые свойства
}

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


<div class="todo-item" v-for="item in TODOS"></div>

Обратите внимание, как мы использовали оператор распространения ES6 [...] внутри наших вычисленных свойств.


ХРАНИЛИЩЕ VUEX НЕ ПРОСТО ИСТОЧНИК ТЕКУЩЕГО СОСТОЯНИЯ ВАШЕГО ПРИЛОЖЕНИЯ. ОНО ТАКЖЕ ЯВЛЯЕТСЯ ЕДИНСТВЕННОЙ ТОЧКОЙ, КОТОРАЯ ДОЛЖНА ИЗМЕНЯТЬ ЭТО СОСТОЯНИЕ.


Это требует небольшого объяснения. Мы уже научились создавать действия для получения и установки элементов списка дел в нашем хранилище. Что делать, если нам нужно обновить элемент и пометить его? Где мы запускаем код для этого?


В Интернете вы можете найти различные мнения на этот счет. В документации также нет четких указаний касаемо этого.


Я бы рекомендовал хранить все вызовы API внутри действий в вашем хранилище Vuex. Таким образом, каждое изменение состояния, происходит только внутри хранилища, тем самым облегчая отладку и упрощая понимание кода, а также делает редактирование кода более легким.



Организация кода


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


Создайте новую директорию внутри вашего хранилища и назовите ее modules. Добавьте в созданную директорию файл todos.js, содержащий следующий код:


const state = {};
const getters = {};
const mutations = {};
const actions = {};

export default {
  state,
  getters,
  mutations,
  actions,
};

Теперь мы можем переместить переменные состояния, геттеры, мутации и действия из файла index.js в файл todos.js. Не забудьте импортировать Axios. Все, что нам нужно сделать, это дать знать Vuex о том, что мы создали модуль хранилища и где его можно найти. Обновленный файл index.js должен выглядеть примерно так:


import Vue from 'vue';
import Vuex from 'vuex';
import Axios from 'axios';
import todos from './modules/todos';

Vue.use(Vuex);

export const store = new Vuex.Store({
  state: {},
  getters: {},
  mutations: {},
  actions: {},
  modules: {
    todos,
  },
});

Файл todos.js будет выглядеть так:


import Axios from 'axios';

state = {
  todos: null,
};

getters = {
  TODOS: state => {
    return state.todos;
  },
};

mutations = {
  SET_TODO: (state, payload) => {
    state.todos = payload;
  },

  ADD_TODO: (state, payload) => {
    state.todos.push(payload);
  },
};

actions = {
  GET_TODO: async (context, payload) => {
    let {data} = await Axios.get('http://yourwebsite.com/api/todo');
    context.commit('SET_TODO', data);
  },

  SAVE_TODO: async (context, payload) => {
    let {data} = await Axios.post('http://yourwebsite.com/api/todo');
    context.commit('ADD_TODO', payload);
  },
};

export default {
  state,
  getters,
  mutations,
  actions,
};


Резюме


  1. Состояние приложения хранится как один большой JSON-объект.
  2. Геттеры используются для доступа к значениям, находящимся в хранилище.
  3. Мутации обновляют ваше состояние. Следует помнить, что мутации являются синхронными.
  4. Все асинхронные операции должны выполняться внутри действий. Действия изменяют состояние, инициируя мутации.
  5. Возьмите за правило инициировать мутации исключительно через действия.
  6. Модули могут использоваться для организации вашего хранилища в нескольких небольших файлах.

Vuex делает работу с Vue намного проще и веселее. Если вы новичок, могут возникать ситуации, когда вам сложно решить, нужно ли использовать Vuex в определенных областях вашего приложения. Следуйте своему инстинкту. Вы достигнете высокой скорости довольно быстро.

Комментарии (14)


  1. Djaler
    28.08.2018 20:23
    +1

    А зачем делать геттеры для простых свойств? Я всегда считал, что во Vuex геттеры нужно применять для сложных объектов


    1. Craftworker Автор
      29.08.2018 08:52

      В статье мы наблюдаем демонстрацию того, что это возможно при использовании Vuex.


      1. Djaler
        29.08.2018 13:09

        ну тогда надо было указать, что к state можно получить доступ и без геттеров


    1. AmdY
      30.08.2018 00:03

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


  1. XAHTEP26
    28.08.2018 22:21

    В коде из раздела «Но почему?» ошибка в действии «SET_NAME»:

    context.dispatch('SET_NAME', name);

    Тут должно быть не dispatch, a commit — иначе получается какая-то рекурсия.


    1. Craftworker Автор
      29.08.2018 08:46

      Да, вы совершенно правы. Исправлено.


  1. XAHTEP26
    28.08.2018 22:22

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


    1. HAGer2000
      29.08.2018 08:49

      Все правильно. Я думаю, тут скорее продемонстрированы возможности vuex-а


      1. Craftworker Автор
        29.08.2018 08:51

        Всё верно.


  1. Suliman
    29.08.2018 08:53

    Зачем для хранения данных использовать Vuex (самому писать геттеры, сеттеры, даже для простейшего получения значения и установки значения) если есть localStorage. Почему не использовать localStorage для хранения данных?


    1. Craftworker Автор
      29.08.2018 08:57

      Будет ли работать реактивность в случае с использованием localStorage?


    1. XAHTEP26
      29.08.2018 09:23

      Ну во первых изменение состояние Vuex автоматически вызывает изменения в DOM, а в LocalStorage отследить состояние можно только отследив событие «storage», причем это событие не сработает если оно вызвано кодом исполняемым в той же вкладке.

      Во вторых в LocalStorage можно хранить только строки, а чтобы поместить туда хотябы простейший объект — его надо сериализовать.

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

      Ну и еще много причин. Если в кратце — то это разные механизмы, предназначенные для разных задач и заменить одно другим нельзя.


  1. Fragster
    29.08.2018 11:48

    А я все жду, когда же они разродятся вот этой фичей из роадмапа:

    Simplify usage
    • Getting rid of mapXXX helpers via scoped-slot based store consumer component
    • Getting rid of the need for separating actions and mutations


  1. LiguidCool
    29.08.2018 13:04

    «Посмотрите на Redux, а теперь на Vuex, снова на Redux и снова на Vuex. Да, Vuex на коне!»