Перевод статьи подготовлен в преддверии старта курса «Vue.js-разработчик».
В основе масштабного приложения на Vue.js лежит хранилище, в котором находятся все данные. Хранилище Vuex в приложении на Vue.js служит единым источником истины, который обеспечивает отличную производительность и реактивность из коробки. По мере того, как обрастает кодом ваше приложение, количество данных в хранилище Vuex также растет, и управлять ими становится труднее. Проектирование управления состоянием вашего приложения на основе лучших практик поможет решить большинство проблем, которые появляются по мере усложнения приложения.
В этой статье мы поговорим о некоторых лучших практиках и советах по проектированию архитектуры механизма управления состоянием в масштабном приложении на Vue.js. Мы поговорим о следующих пяти концепциях, которые помогут спроектировать хранилище наилучшим образом.
Хранилище Vuex состоит из четырех компонентов:
Если вы еще не знакомы с этими четырьмя концепциями, то сейчас мы вкратце о них поговорим. Объект состояния хранит данные вашего приложения в виде большого JSON-файла. Функция getter помогает вам получить доступ к этим объектам состояния извне, она может вести себя как реактивное вычисляемое свойство. Мутации, как следует из названия, используются для мутации/изменения объекта состояния. Действия очень похожи на мутации, однако, вместо того чтобы изменять состояние, действие коммитят мутации. Действия могут содержать любой асинхронный код или бизнес-логику.
Vuex рекомендует, чтобы объект состояния изменялся только внутри функции-мутации. Также желательно не запускать какой-либо тяжелый или блокирующий код внутри функции-мутации, так как по своей природе он синхронный. Вместо этого нужно использовать действия, которые должны быть спроектированы асинхронно для выполнения тяжелых работ или выполнения сетевых запросов и коммитов мутаций. Действия являются лучшим местом для хранения бизнес-логики и логики обработки данных. Действия считаются лучшими кандидатами на эту роль, потому что вы можете их использовать для отправки данных в хранилище или для отправки данных непосредственно в компоненты Vue.
Хорошей практикой является отказ от прямого доступа к объекту состояния и использование вместо него getter’а. Функцию getter можно легко смаппить в любой компонент Vue с помощью mapGetter, в качестве вычисляемых свойств.
Неудивительно, что с увеличением размера и сложности хранилище захламляемся и становится более трудным к пониманию. Vuex из коробки предоставляет возможность разделить хранилище на отдельные модули согласно целям в вашем приложении. Разделение бизнес-логики с помощью модульного хранилища повышает удобство сопровождения приложения. Поэтому нам нужно убедиться, что каждый модуль вынесен в отдельное пространство имен, а не обращаться к ним с помощью глобального контекста хранения.
Вот небольшой пример создания модуля хранилища и того, как объединить все модули в основном хранилище.
Структура каталога
Обратите внимание, что каждый модуль назван по принципу
Авторинг модулей
Мы можем переместить сетевые вызовы в отдельные JavaScript-файлы, но об этом мы поговорим в другой статье, которая будет посвящена архитектуре сетевого уровня приложения. Мы даже можем разделить объекты состояния, getter’ы, действия и мутации на разные файлы для удобства чтения. Хорошо хранить все связанные функций в одном месте и разбивать хранилище на модули все дальше и дальше, если оно все еще слишком большое и сложное.
Комбинирование модулей
Как я уже говорил, если модули становятся все сложнее и больше, их нужно разделить на подмодули для уменьшения сложности. Когда количество модулей увеличивается, управлять этими модулями по отдельности становится действительно сложно, а в особенности импортировать каждый из них. У нас будет небольшой JS-файл в каталоге modules, который сделает эту работу за нас. Он поможет нам объединить вместе все модули.
Чтобы он заработал, рекомендуется следовать строгой структуре именования файлов модулей. В конечном итоге, наличие стандарта именования повысит удобство поддержки всего проекта. Для упрощения работы модули можно назвать в нотации camelCase, и добавить расширение
Скрипт для автоматического экспорта
Теперь, когда наш скрипт для автоматического экспорта находится там, где нужно, мы можем начать импорт в основное хранилище и получить доступ ко всем модулям.
Как только вы использовали автоматический импорт в основном хранилище, все новые модули, добавляемые в подкаталог modules будут автоматически импортированы. Например, если у вас есть файл с именем
Если вы когда-нибудь работали с приложениями на Vue + Vuex, которые обрабатывают большое количество данных, то, возможно, сталкивались со сценарием, когда нужно было сбросить состояние хранилища. Функция сброса довольно распространенная механика. В случае, когда в вашем приложении есть аутентификация пользователя, вы можете сбросить хранилище при выходе пользователя из приложения.
Чтобы сбросить хранилище, нужно перевести объект состояния в исходное состояние и скопировать его основное состояние. Для этого можно просто использовать функцию, которая возвращает начальное состояние. В модуле хранилища создайте функцию
Теперь у нас есть отдельное изначальное состояние, и любые изменения, которые мы произведем с состоянием, не повлияют на фактическое изначальное состояние. Таким образом, мы можем использовать полученную функцию для сброса хранилища. Создайте функцию-мутацию, которая с помощью изначального состояния изменит весь объект хранилища.
Как только у нас получилась мутация RESET, мы можем использовать ее для сброса хранилища вызвав действие, либо закоммитив мутацию RESET.
Что делать, если нам нужно сбросить хранилище целиком? Вместе со всеми модулями? Если вы выполнили 4-й и 5-й пункт настройки автоматического импорта и мутации сброса состояния модуля для всех ваших модулей, мы можем использовать следующее действие в файле основного хранилища, чтобы сбросить все модули одновременно.
Обратите внимание, что действие, которое мы создали, находится в основном файле хранилища, а не внутри какого-то конкретного модуля. Это действие можно запустить из любого места вашего компонента Vue с помощью следующей строки кода:
Вам понравилась статья? В следующих статьях мы более подробно обсудим аспекты сетевой архитектуры нашего приложения на Vue.js. Мы поговорим о методах, которые используются для управления учетными данными при аутентификации, перехватчиках и обработке ошибок в сетевых запросах.
Чтобы узнать больше о том, чем мы занимаемся в Locale.ai, прочтите здесь о неизведанных землях геопространственной аналитики.
Особую благодарность мы выражаем Крису Фритсу за его удивительный рассказ о 7 вещах, которые скрывают от вас консультанты Vue. Он подкинул нам несколько идей, которые мы использовали в этой статье.
Узнать о курсе подробнее.
В основе масштабного приложения на Vue.js лежит хранилище, в котором находятся все данные. Хранилище Vuex в приложении на Vue.js служит единым источником истины, который обеспечивает отличную производительность и реактивность из коробки. По мере того, как обрастает кодом ваше приложение, количество данных в хранилище Vuex также растет, и управлять ими становится труднее. Проектирование управления состоянием вашего приложения на основе лучших практик поможет решить большинство проблем, которые появляются по мере усложнения приложения.
В этой статье мы поговорим о некоторых лучших практиках и советах по проектированию архитектуры механизма управления состоянием в масштабном приложении на Vue.js. Мы поговорим о следующих пяти концепциях, которые помогут спроектировать хранилище наилучшим образом.
- Структурирование хранилища
- Разделение хранилища на модули
- Автоматический импорт модулей хранилища
- Сброс состояния модуля
- Глобальный сброс состояния модуля
Структурирование хранилища
Хранилище Vuex состоит из четырех компонентов:
- Объекта состояния
- Функции getter
- Действий
- Мутаций
Если вы еще не знакомы с этими четырьмя концепциями, то сейчас мы вкратце о них поговорим. Объект состояния хранит данные вашего приложения в виде большого 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. Он подкинул нам несколько идей, которые мы использовали в этой статье.
Узнать о курсе подробнее.
DmitryKazakov8
Не использую VUEX в работе, так как в целом не нравится добавление дополнительного слоя в виде «мутаций». Можно вполне упростить этот код, который еще и в строках передает названия функций (facepalm)
до
Экшены — уже часть экосистемы стора, и если в них не проводить изменения данных, то смысла в них нет. Логично было бы оставить либо actions, либо mutations. Разработчики данной библиотеки аргументируют таким вот странным способом:
Делает явным? Ничуть, лишь удлиняет путь и размазывает логику по нескольким сущностям. Отслеживание каждой мутации, то есть вызовов мутирующих функций? В реальных приложениях это не нужно — могут быть задачи отследить, что изменился определенный параметр в сторе и предпринять соответствующие действия, а вот знать, что его изменила функция SET_VARIABLE_1 или SET_VARIABLE_ALL бывает нужно только для дебага очень запутанных приложений и можно так же быстро отловить обычным console.log. Тайм тревелинг — маркетинговая фича, добравшаяся до умов разработчиков, но реального применения у нее нет, так как если нужно сделать к примеру историю редактирования документа, то никто в здравом уме не будет полагаться на «историю действий» вместо «истории состояний». А к созданию «снимка состояний» добавление дополнительного слоя в виде mutations не имеет никакого отношения, в общем, очень слабая аргументация, берущая лишь продающим стилем изложения.
По статье — решение использовать require.context не лучшее, особенно с преобразованием имен. Таким образом ни IDE, ни человек не сможет понять, что же сейчас находится в export default modules — поможет только console.log + чтение документации к архитектуре, если она написана, а это дополнительные затраты времени. Лучше генерировать классический реэкспортный файл.
Ну и пол-статьи про то, как заменить один объект другим объектом, создав для этого функцию — как скучно, наверно, было заниматься переводом)