Архитектура управления состоянием в крупном приложении на VueJs

Архитектура управления состоянием в крупном приложении на VueJs

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

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

Vue.js предоставляет свой собственный официальный шаблон управления состоянием и библиотеку, именуемую Vuex, и это одна из наиболее рекомендуемых библиотек, которая должна использоваться для управления состоянием в приложениях Vue.js. Здесь я не буду детально объяснять все базовые понятия Vuex, что такое vuex, мутации или экшены и т.д. В дальнейшем я напишу отдельную статью, в которой подробно расскажу об этом. А сейчас сконцентрируемся на архитектуре.

Когда мы говорим о состоянии в компонентном приложении, то, как правило, оно имеет 2 типа:

  • Локальное состояние компонента, ограниченное рамками одного компонента
  • Общее (глобальное) состояние приложения, доступное для всех компонентов приложения

Некоторые люди предлагают хранить все состояния (локальные и глобальные) в общем хранилище состояния приложения, в то время как другие предлагают управлять локальными состояниями компонента самим компонентом, а не хранить все возможные состояния в глобальном хранилище приложения. Я считаю, что 2-й подход - это более правильный способ управления состоянием, он делает хранилище приложения более чистым и управляемым.

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

В этой структуре проекта каждый функциональный модуль имеет папку shared/state. Это именно то место, где мы будем хранить все файлы состояний, относящиеся к данному функциональному модулю. Например, для модуля users будет создан отдельный файлы состояний в папке users/shared/state и предположим, что он будет содержать такие состояния, как список пользователей, подробная информация о каждом из них и т.д. После чего, для каждого состояния мы создадим по одному файлу, скажем, для списка пользователей мы создадим файл users-data.js в папке users/shared/state, и каждый файл будет выглядеть так, как описано ниже:

import { reflectKeys } from '@/app/shared/services';

import { fetchUsers } from '../services';

/** Начальное состояние */
const initialState = {
  loading: false,
  data: null,
  error: null
};

/** Префикс модуля, для мутаций и экшенов */
const namespacedPrefix = '[USERS]';

/**
 * Типы мутаций
 */
const mutationTypes = reflectKeys(
  [
    'USERS_DATA_SUCCESS',
    'USERS_DATA_REQUEST',
    'USERS_DATA_ERROR',
    'USERS_DATA_RESET'
  ],
  namespacedPrefix
);

const {
  USERS_DATA_ERROR,
  USERS_DATA_REQUEST,
  USERS_DATA_RESET,
  USERS_DATA_SUCCESS
} = mutationTypes;

/**
 * Мутации пользовательской информации
 */
const mutations = {
  /** запрос получения пользователя */
  [USERS_DATA_REQUEST](state) {
    Object.assign(state, { loading: true, error: null });
  },

  /** успешное получение информации о пользователе */
  [USERS_DATA_SUCCESS](state, payload) {
    Object.assign(state, { loading: false, data: payload });
  },

  /** ошибка получения информации о пользователе */
  [USERS_DATA_ERROR](state, payload) {
    Object.assign(state, {
      loading: false,
      data: null,
      error: payload || true
    });
  },

  /** очистка данных, возвращаем состояние к исходным данных */
  [USERS_DATA_RESET](state) {
    Object.assign(state, ...initialState);
  }
};

/** Константы для типов экшенов */
export const actionsTypes = reflectKeys(['FETCH_USER_DATA'], namespacedPrefix);

/**
 * Экшены пользователя
 */
const actions = {
  /** Получение список пользователей */
  async [actionsTypes.FETCH_USER_DATA](context, authCred) {
    context.commit(USERS_DATA_REQUEST);

    const result = await fetchUsers(authCred).catch(e => {
      context.commit(USERS_DATA_ERROR, e);
    });

    if (result) {
      context.commit(USERS_DATA_SUCCESS, result);
    }

    return result;
  }
};

export default {
  mutations,
  actions,
  state: initialState
};

Есть несколько ключевых моментов, которые я хотел бы объяснить насчёт вышеуказанного кода:

  • Я поместил все мутации, экшены и связанные с ними константы в один файл, так как они связаны друг с другом. Мы, конечно же, можем хранить их в отдельных файлах, но я подумал, что было бы удобнее разместить их все в одном.
  • reflectKeys - это просто метод, который зеркалирует ключи, т.е. возвращает объект с переданной строкой в качестве ключа и значения. Также, мы можем добавить префикс к значениям ключей. Он просто применяется для создания констант для типов мутаций и экшенов, используя префикс, а не для жесткого программирования строк.
  • Vuex предоставляет собственный способ именования пространства имен состояния, мутаций, экшенов и т.д., но я предпочитаю использовать другой способ именования пространства имен, используя префикс. Мне он показался более подходящим и удобным, потому что нам не везде нужно добавлять пространство имён для вызова действий, мутаций, геттеров состояния и т.д. Но вы можете использовать пространство имён vuex, как рекомендовано в документации.
  • В Vuex мы можем выполнять мутации напрямую, без экшенов. Экшены нужны только для выполнения асинхронных операций. Но, вместо этого я предлагаю использовать экшены в любом случае, инкапсулируя прямой вызов мутаций из клиентского кода. И теперь, чтобы выполнить мутацию напрямую, как при синхронных операциях, так и для асинхронных, мы оборачиваем их в свой отдельный экшен. Вы можете увидеть, что я только что экспортировал типы экшенов, а не типы мутаций. Это связано с тем, что они будут поддерживать консистентность данных, и только экшены будут отвечать за обновление состояния. Так мы будем иметь больше контроля над изменением данных, ведь точка изменения состояния будет всего одна.
  • Как вы видите, экшены - просто выполняют мутации, вся бизнес-логика, например, такая, как вызов API, перемещается в сервисы. Потому что точно так же, нам нужно поддерживать экшены тонкими и хранить всю бизнес-логику в сервисах. Это делает приложение более гибким, предоставляя чёткое разделение на бизнес-логику и управление состоянием.

Каждый функциональный модуль имеет файл состояния в корневом каталоге модуля. Этот файл отвечает за объединение всех состояний этого функционального модуля. Например, в нашем случае это src/app/users/user-state.js. Мы объединяем все состояния этого функционального модуля с помощью Vuex-модулей и код выглядим так, как показано ниже:

import { usersData } from './shared/state';

export default {
  modules: {
    usersData
  }
};

Точно так же состояние всех функциональных модулей будет объединено воедино на верхнем уровне приложения с использованием одних и тех же концепций, т.е. модулей Vuex. Например, src/app/app-state.js будет выглядеть так:

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

import { usersState } from './users';

Vue.use(Vuex);

export default new Vuex.Store({
  state: {},
  mutations: {},
  actions: {},
  modules: {
    usersState
  }
});

Как вы можете заметить, я импортирую userState из функционального модуля users.

Бывают также случаи, когда нам необходимо разделить состояния между различными функциональными модулями. Подобный тип состояния может быть определен в app/shared/state, а затем должен быть подключён и в общее состояние приложения.

Весь готовый шаблон подобной архитектуры, вы можете найти по ссылке

Заключение:

Управление состоянием в масштабном приложении является очень тонким местом и нуждается в чётко и определенной архитектуре. Архитектура, описанная в этой статье, является общей и работает в большинстве случаев проектирования больших приложений, но она может варьироваться в зависимости от типа приложения и потребностей разработчика.

Надеюсь, что теперь вы знаете, как хранить и управлять состоянием в крупном приложении на Vue.js. А так же, как создавать архитектуру, где доступно несколько вариантов хранения состояния в приложении на Vue.js - общее для приложения (shared state) и локальной для компонента.