Перевод статьи подготовлен в преддверии старта курса «Vue.js-разработчик».




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

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

  1. Структурирование хранилища
  2. Разделение хранилища на модули
  3. Автоматический импорт модулей хранилища
  4. Сброс состояния модуля
  5. Глобальный сброс состояния модуля

Структурирование хранилища


Хранилище Vuex состоит из четырех компонентов:

  1. Объекта состояния
  2. Функции getter
  3. Действий
  4. Мутаций

Если вы еще не знакомы с этими четырьмя концепциями, то сейчас мы вкратце о них поговорим. Объект состояния хранит данные вашего приложения в виде большого JSON-файла. Функция getter помогает вам получить доступ к этим объектам состояния извне, она может вести себя как реактивное вычисляемое свойство. Мутации, как следует из названия, используются для мутации/изменения объекта состояния. Действия очень похожи на мутации, однако, вместо того чтобы изменять состояние, действие коммитят мутации. Действия могут содержать любой асинхронный код или бизнес-логику.

Vuex рекомендует, чтобы объект состояния изменялся только внутри функции-мутации. Также желательно не запускать какой-либо тяжелый или блокирующий код внутри функции-мутации, так как по своей природе он синхронный. Вместо этого нужно использовать действия, которые должны быть спроектированы асинхронно для выполнения тяжелых работ или выполнения сетевых запросов и коммитов мутаций. Действия являются лучшим местом для хранения бизнес-логики и логики обработки данных. Действия считаются лучшими кандидатами на эту роль, потому что вы можете их использовать для отправки данных в хранилище или для отправки данных непосредственно в компоненты Vue.

Хорошей практикой является отказ от прямого доступа к объекту состояния и использование вместо него getter’а. Функцию getter можно легко смаппить в любой компонент Vue с помощью mapGetter, в качестве вычисляемых свойств.

Разделение хранилища на модули


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

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

Структура каталога

store/
   +-- index.js    ---> Main Store file
   L-- modules/
       +-- module1.store.js
       +-- module2.store.js
       +-- module3.store.js
       +-- module4.store.js
       +-- module5.store.js
       L-- module6.store.js

Обратите внимание, что каждый модуль назван по принципу ModuleName.store.js, что поможет в будущем при автоматическом импорте этих модулей, но об этом мы поговорим в следующем разделе.

Авторинг модулей

Мы можем переместить сетевые вызовы в отдельные JavaScript-файлы, но об этом мы поговорим в другой статье, которая будет посвящена архитектуре сетевого уровня приложения. Мы даже можем разделить объекты состояния, getter’ы, действия и мутации на разные файлы для удобства чтения. Хорошо хранить все связанные функций в одном месте и разбивать хранилище на модули все дальше и дальше, если оно все еще слишком большое и сложное.

/* Module1.store.js */

// State object
const state = {
    variable1: value,
    variable2: value,
    variable3: value
}


// Getter functions
const getters = {
    getVariable1( state ) {
       return state.variable1;
    },
    getVariable2( state ) {
       return state.variable2;
    },
    ....
}


// Actions 
const actions = {
    fetchVariable1({ commit }) {
        return new Promise( (resolve, reject) => {
               // Make network request and fetch data
               // and commit the data
               commit('SET_VARIABLE_1', data); 
               resolve();
        }
    },
    ....
}
// Mutations
const mutations = {
    SET_VARIABLE_1(state, data) {
       state.variable1 = data;
    },
    SET_VARIABLE_2(state, data) {
       state.variable2 = data;
    },
    ....
}
export default {
    namespaced: true,
    state,
    getters,
    actions,
    mutations
}

Комбинирование модулей

/** store/index.js **/
import Vue from 'vue';
import Vuex from 'vuex';
import createLogger from 'vuex/dist/logger';
import Module1 from './modules/module1.store';
import Module2 from './modules/module2.store';
...
Vue.use(Vuex);
const debug = process.env.NODE_ENV !== 'production';
export default new Vuex.Store({
   modules: {
      Module1,
      Module2,
      ...
   },
   strict: debug,
   plugins: debug? [ createLogger() ] : [],
}

Автоматический импорт модулей хранилища


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

Чтобы он заработал, рекомендуется следовать строгой структуре именования файлов модулей. В конечном итоге, наличие стандарта именования повысит удобство поддержки всего проекта. Для упрощения работы модули можно назвать в нотации camelCase, и добавить расширение .store.js. Например, для файла userData.store.js нам нужно будет добавить файл index.js внутрь подкаталога модулей, чтобы найти все нужные модули и экспортировать их в основное хранилище.

store/
   +-- index.js    ---> Main Store file
   L-- modules/
       +-- index.js   --> Auto exporter
       +-- module1.store.js
       L-- module2.store.js

Скрипт для автоматического экспорта

/**
 * Automatically imports all the modules and exports as a single module object
 */
const requireModule = require.context('.', false,  /\.store\.js$/);
const modules = {};

requireModule.keys().forEach(filename => {

    // create the module name from fileName
    // remove the store.js extension and capitalize
    const moduleName = filename
                   .replace(/(\.\/|\.store\.js)/g, '')
                   .replace(/^\w/, c => c.toUpperCase())

    modules[moduleName] = requireModule(filename).default || requireModule(filename);
});

export default modules;

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

import Vue from 'vue'
import Vuex from 'vuex'
import createLogger from 'vuex/dist/logger'

// import the auto exporter
import modules from './modules';

Vue.use(Vuex);
const debug = process.env.NODE_ENV !== 'production';

export default new Vuex.Store({
  modules, // all your modules automatically imported :)
  strict: debug,
  plugins: debug ? [createLogger()] : [] // set logger only for development
})

Как только вы использовали автоматический импорт в основном хранилище, все новые модули, добавляемые в подкаталог modules будут автоматически импортированы. Например, если у вас есть файл с именем user.store.js, он будет импортирован как модуль хранения в пространстве имен User. Вы можете использовать это пространство имен для маппинга getter’ов и действий в компоненты с помощью mapGetters и mapActions.

Сброс состояния модуля


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

Чтобы сбросить хранилище, нужно перевести объект состояния в исходное состояние и скопировать его основное состояние. Для этого можно просто использовать функцию, которая возвращает начальное состояние. В модуле хранилища создайте функцию initialState(), пусть она возвращает текущий объект состояния.

const initialState = () => ({
    variable1: value,
    variable2: value,
    variable3: value
});

const state = initialState();

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

const initialState = () => ({
    variable1: value,
    variable2: value,
    variable3: value
});

const state = initialState();

// Getters

// Actions

// Mutations
const mutations = {
    RESET(state) {
      const newState = initialState();
      Object.keys(newState).forEach(key => {
            state[key] = newState[key]
      });
    },
    // other mutations
}
	

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

// Actions
const actions = {
   reset({ commit }) {
       commit('RESET');
   },
}

Глобальный сброс состояния модуля


Что делать, если нам нужно сбросить хранилище целиком? Вместе со всеми модулями? Если вы выполнили 4-й и 5-й пункт настройки автоматического импорта и мутации сброса состояния модуля для всех ваших модулей, мы можем использовать следующее действие в файле основного хранилища, чтобы сбросить все модули одновременно.

import Vue from 'vue'
import Vuex from 'vuex'
import createLogger from 'vuex/dist/logger'
import modules from './modules';

Vue.use(Vuex);
const debug = process.env.NODE_ENV !== 'production';

export default new Vuex.Store({
  modules,
  actions: {
    reset({commit}) {
      // resets state of all the modules
      Object.keys(modules).forEach(moduleName => {
        commit(`${moduleName}/RESET`);
      })
    }
  },
  strict: debug,
  plugins: debug ? [createLogger()] : [] // set logger only for development
});

Обратите внимание, что действие, которое мы создали, находится в основном файле хранилища, а не внутри какого-то конкретного модуля. Это действие можно запустить из любого места вашего компонента Vue с помощью следующей строки кода:

this.$store.dispatch('reset');

Что дальше?


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

Чтобы узнать больше о том, чем мы занимаемся в Locale.ai, прочтите здесь о неизведанных землях геопространственной аналитики.

Особую благодарность мы выражаем Крису Фритсу за его удивительный рассказ о 7 вещах, которые скрывают от вас консультанты Vue. Он подкинул нам несколько идей, которые мы использовали в этой статье.



Узнать о курсе подробнее.