Vuex — это официальная, отлично документированная библиотека для управления состоянием приложений, разработанная специально для фреймворка Vue.js. Автор материала, перевод которого мы сегодня публикуем, полагает, что пользоваться этой библиотекой гораздо приятнее, чем Redux, так как, во-первых, для работы с Vuex требуется меньше шаблонного кода, а во-вторых — из-за того, что для работы с асинхронными механизмами здесь не нужно дополнительных библиотек. Более того, так как библиотека Vuex создана той же командой, которая занимается работой над Vue, эта библиотека очень хорошо интегрируется с данным фреймворком. К сожалению, в работе с Vuex всё ещё можно столкнуться с одной сложностью, которая заключается в правильной подготовке структуры проектов, в которых планируется пользоваться этой библиотекой.
В этой статье вы найдёте описание методики структурирования больших проектов, использующих Vuex, и скрипта, предназначенного для автоматизации процесса создания модулей Vuex.
Шаблон vue-enterprise-boilerplate и проблема структуры проекта
Один из разработчиков Vue, Крис Фриц, создал отличный шаблон для Vue, структура проекта, представленная в котором, рассчитана на использование Vuex. В частности, этот шаблон позволяет Vuex автоматически регистрировать модули, основываясь на файлах, находящихся в папке modules
. Структура папок проекта может выглядеть примерно так, как показано на следующем рисунке.
Структура проекта и неудобное размещение кода
При использовании этого шаблона нужно, чтобы состояние, геттеры, действия и мутации находились бы в одном файле. Лично я предпочитаю держать их в отдельных файлах, что позволяет, учитывая то, что модули Vuex иногда бывают довольно большими, удобно ориентироваться в программах, обходясь без необходимости прокручивать огромные куски кода. Следуя этой идее, изменим код из шаблона таким образом, чтобы то, что относится к отдельным модулям, можно было бы разложить по папкам, предназначенным для этих модулей. То есть, структура проекта изменится и будет похожа на ту, что показана ниже.
Структура проекта с разбиением материалов модулей по отдельным файлам, которые находятся в папках модулей
Разработка шаблона, поддерживающего удобную структуру проекта
Итак, мы собираемся организовать работу с Vuex так, чтобы можно было бы использовать в своих проектах структуры папок и файлов, подобные той, которая показана на предыдущем рисунке. Для того чтобы это сделать, сначала создадим новый проект с использованием Vue CLI 3.
После того, как у вас будет шаблон проекта, готовый к дальнейшей работе с ним, установите Vuex и Lodash, выполнив в терминале команду npm install vuex lodash -save
. Для работы с модулями нам понадобится функция camelCase
из Lodash, которая предназначена для преобразования строк к верблюжьему стилю.
Теперь создадим папку и структуру файлов, похожую на ту, которая показана на предыдущем рисунке.
Начнём работу с файла store.js
. Вот его код:
import Vue from 'vue'
import Vuex from 'vuex'
import modules from './modules'
Vue.use(Vuex)
const store = new Vuex.Store({
modules,
strict: process.env.NODE_ENV !== 'production'
})
// Автоматически запустить действие `init` для каждого существующего модуля
for (const moduleName of Object.keys(modules)) {
if (modules[moduleName].actions.init) {
store.dispatch(`${moduleName}/init`)
}
}
export default store
Здесь импортируются Vue и Vuex, так как без них нам не обойтись. Кроме того, мы импортируем модули из /modules/index.js
. Далее, мы инициализируем хранилище и проходимся в цикле по всем модулям. Если у модуля есть действие init
, мы инициализируем модуль. Это оказывается очень кстати для тех модулей, которые нужно инициализировать при запуске приложения. В итоге, конечно, мы экспортируем store
, после чего, обычно, это импортируется в файл main.js
и добавляется к экземпляру Vue.
Теперь пришло время поработать с файлом index.js
, который находится в папке /store/modules
.
// Будем регистрировать модули Vuex с использованием имён их файлов, приведённых к верблюжьему стилю.
import camelCase from 'lodash/camelCase';
// Получаем все файлы
const requireModule = require.context(
// Ищем файлы в текущей директории
'.',
// Ищем файлы в поддиректориях
true,
// Исключаем файл index.js, равно как и файлы, в именах которых
// есть строки 'actions', 'mutations', или 'getters' .
// Кроме того, включаем только файлы с расширением .js
/^(?!.*(actions|mutations|getters|index)).*.js$/
);
const modules = {};
requireModule.keys().forEach(fileName => {
// Игнорируем файлы модульных тестов
if (/.unit.js$/.test(fileName)) return;
// Избавляемся от расширений файлов для преобразования их имён к верблюжьему стилю
modules[camelCase(fileName.split('/')[1].replace(/(./|.js)/g, ''))] = {
namespaced: true,
...requireModule(fileName).default
};
});
export default modules;
В этом коде мы сначала импортируем функцию camelCase
из Lodash. Затем используем метод require.context
для подключения модулей. В качестве третьего параметра мы передаём туда регулярное выражение, которое отфильтрует файл index.js
, а также файлы, в именах которых есть строки actions
, mutations
и getters
. Они будут импортированы в файл состояния, например, в auth.js
, а затем экспортированы. Например, вот как файл auth.js
из папки src/store/modules/auth/
может выглядеть в начале работы:
import actions from './actions';
import mutations from './mutations';
import getters from './getters';
const state = {
user: null
};
export default {
state,
mutations,
getters,
actions
};
Теперь осталось лишь пройтись по всем модулям и сформировать единственный объект со всеми ними. Тут нужно исключить все файлы, в имени которых есть строка unit
, так как они нужны только для тестов, а не для разработки или для разворачивания проекта в продакшне. После этого мы добавляем к объекту modules
новое свойство, у которого будет имя файла состояния, например — auth
или users
. Кроме того, мы используем функцию camelCase
для того, чтобы имена свойств выглядели бы единообразно. Затем мы заполняем объект modules
, перебирая requireModule
и пользуясь конструкцией ...requireModule(fileName).default
, после чего экспортируем modules
.
Собственно говоря, именно так и можно структурировать проект, в котором состояние, геттеры, действия и мутации хранятся раздельно и оказываются удобно организованными. Теперь поговорим о том, как написать скрипт для автоматического создания модулей Vuex.
Скрипт для автоматического создания модулей Vuex
Создадим в папке проекта новую папку с именем scripts
, в ней создадим файл generateVuexModule.js
. Для этого проекта нам понадобится Node.js, поэтому, если у вас эта платформа не установлена — сейчас самое время это исправить. У нашего скрипта есть лишь одна зависимость — пакет chalk
, который используется для оформления материалов, выводимых в консоль. Установить этот пакет можно командой npm install -save-dev chalk
.
▍Шаг 1
В файле generateVuexModule.js
нужно подключить три модуля: fs
, path
и chalk
. Также тут нужна константа с путём к папке с модулями (src/store/modules
) и ещё одна константа — args
, в которую попадут аргументы, переданные скрипту при его запуске.
const fs = require('fs');
const path = require('path');
const chalk = require('chalk');
const modulesPath = 'src/store/modules';
const args = process.argv.slice(2);
const error = (...args) => {
console.log(chalk.red(...args));
};
const success = (...args) => {
console.log(chalk.green(...args));
};
if (!args.length) {
error('You must provide a name for the module!');
return;
}
Как видите, мы записываем в args
все аргументы кроме первых двух, так как они представляют собой путь к node.exe
и к файлу скрипта, а эти сведения нам не нужны. Мы заинтересованы лишь в третьем параметре — имени нового модуля. Кроме того, тут есть пара функций, error
и success
, которые используют вышеупомянутый пакет chalk
для вывода сообщений текстами разных цветов.
Здесь нужно проверить длину массива args
для того, чтобы узнать, передано ли нашему скрипту имя модуля, и, если это не так — выдать сообщение об ошибке. Поэтому, если вы попытаетесь запустить этот скрипт с помощью команды node generateVuexModule.js
, ничего больше ему не передавая, вы увидите в терминале сообщение об ошибке.
▍Шаг 2
К этому моменту у нас есть имя для модуля и путь, заданный константой modulesPath
. Однако с этими данными ещё надо поработать. А именно, извлечь имя из массива args
и собрать полный путь к модулю, не говоря уже о формировании его содержимого.
const moduleName = args[0];
const modulePath = path.join(__dirname, '../', modulesPath, moduleName);
if (fs.existsSync(modulePath)) {
error(`${moduleName} directory already exists!`);
return;
}
const stateContent = `import getters from './getters';
import actions from './actions';
import mutations from './mutations';
const state = {};
export default {
state,
getters,
actions,
mutations
};
`;
const exportFileContent = `import * as types from '@/store/types';
export default {
};
`;
Имя модуля будет находиться в элементе массива args
с индексом 0. На данном этапе выполнения программы мы можем рассчитывать на наличие этого элемента, так как ранее мы сделали попытку извлечь его из process.argv
, после чего проверили длину массива args
. Кроме того, мы подготовили полный путь с использованием модуля path
и метода join
. Текущую директорию мы получили с помощью конструкции __dirname
, перешли на один уровень вверх, так как файл generateVuexModule.js
находится в папке проекта scripts
. Затем мы просто добавляем к тому, что получилось, содержимое константы modulesPath
и имя модуля. К этому моменту в константе modulePath
должно содержаться нечто вроде pathToYourProject/project/src/store/modules/moduleName
. Именно здесь и будет создан модуль. Теперь, так как у нас есть полный путь, мы можем проверить, существует ли данная директория. Нам не хотелось бы случайно перезаписать файлы существующего модуля. В результате, если директория, в которой планируется создать новый модуль, существует, мы выведем, красными буквами, благодаря chalk
, сообщение об ошибке.
Пример сообщения об ошибке
Дальше надо создать константы, в которых будут данные для файлов. Как вы можете предположить, stateContent
используется для файла состояния, то есть, например, для auth.js
, а exportFileContent
— для файлов getters.js
, actions.js
, и mutations.js
. Если нужно, можете добавить в этот список всё, что требуется в вашем проекте.
▍Шаг 3
Теперь нам осталось лишь сформировать пути для файлов модуля и создать их.
const statePath = `${path.join(modulePath, `${moduleName}.js`)}`
const gettersPath = `${path.join(modulePath, 'getters.js')}`
const actionsPath = `${path.join(modulePath, 'actions.js')}`
const mutationsPath = `${path.join(modulePath, 'mutations.js')}`
fs.mkdirSync(modulePath);
fs.appendFileSync(statePath, stateContent);
fs.appendFileSync(gettersPath, exportFileContent);
fs.appendFileSync(actionsPath, exportFileContent);
fs.appendFileSync(mutationsPath, exportFileContent);
success('Module', moduleName, 'generated!');
Сначала мы объявляем четыре константы, каждая из которых содержит путь для соответствующего файла. Далее нам надо создать папку для модуля. Мы уже проверяли, существует ли такая папка и выдавали ошибку в том случае, если это так. Поэтому здесь с созданием папки проблем быть не должно. И, наконец, мы используем команды fs.appendFileSync
, помещая новые файлы с заданным в процессе их создания содержимым в только что созданную директорию. В конце скрипт выводит сообщение об успешном завершении операции.
Для того чтобы воспользоваться этим скриптом, достаточно перейти в терминале в папку scripts
вашего проекта и выполнить команду вида node generateVuexModule.js yourModuleName
. После успешного завершения работы скрипта вы увидите сообщение о создании модуля.
Итоги
Ознакомившись с этим материалом, вы узнали о шаблоне для структурирования больших проектов, в которых планируется использовать Vuex, и о скрипте, упрощающем создание модулей Vuex. Надеемся, эти знания вам пригодятся. Код проекта, примеры из которого мы рассматривали, можно найти здесь.
Уважаемые читатели! Как вы структурируете большие Vue-приложения, в которых используется Vuex?
Автор: ru_vds