Все, кто знаком с Vue, знают, что у Vue-приложения одна точка входа — файл main.js
. Там, помимо создания экземпляра Vue, происходит импорт и своего рода Dependency Injection всех ваших глобальных зависимостей (директив, компонентов, плагинов). Чем больше проект, тем больше становится зависимостей, которые, к тому же, имеют каждая свою конфигурацию. В итоге получим один огромный файл со всеми конфигурациями.
В этой статье речь пойдет о том, как организовать глобальные зависимости, чтобы этого избежать.
Для чего писать это самим?
Многие могут подумать – зачем это нужно, если есть, например, Nuxt, который это сделает за вас? В своих проектах я использовал его тоже, однако в простых проектах это может оказаться избыточным. Кроме того, никто не отменял проекты с legacy-кодом, которые падают на вас, как снег на голову. И подключать туда фреймворк – практически делать его с нуля.
Идейный вдохновитель
Вдохновителем такой организации явился Nuxt. Он был использован мной на крупном проекте с Vue.
У Nuxt есть прекрасная фича – plugins. Каждый плагин – это файл, который экспортирует функцию. В функцию передается конфиг, который также будет передан конструктору Vue при создании экземпляра, а также весь store.
Кроме того, в каждом плагине доступна крайне полезная функция – inject
. Она делает Dependency Injection в корневой экземпляр Vue и в объект store
. А это значит, что в каждом компоненте, в каждой функции хранилища указанная зависимость будет доступна через this
.
Где это может пригодиться?
Помимо того, что main.js
существенно «похудеет», вы также получите возможность использования зависимости в любом месте приложения без лишних импортов.
Яркий пример Dependency Injection – это vue-router. Он используется не так уж и часто – получить параметры текущего роута, сделать редирект, однако это глобальная зависимость. Если он может пригодиться в любом компоненте, то почему бы не сделать его глобальным? К тому же, благодаря этому его состояние тоже будет храниться глобально и меняться для всего приложения.
Другой пример – vue-wait. Разработчики этого плагина пошли дальше и добавили свойство $wait
не только в экземпляр Vue, но и во vuex store. Учитывая специфику плагина, это оказывается крайне полезным. Например, в store есть action, который вызывается в нескольких компонентах. И в каждом случае нужно показать лоадер на каком-то элементе. Вместо того, чтобы до и после каждого вызова action вызывать $wait.start('action')
и $wait.end('action')
, можно просто вызвать эти методы один раз в самом action. И это гораздо более читаемо и менее многословно, чем dispatch('wait/start', 'action' {root: true})
. В случае со store это синтаксический сахар.
От слов к коду
Базовая структура проекта
Посмотрим, как сейчас выглядит проект:
src
- store
- App.vue
- main.js
main.js
выглядит примерно так:
import Vue from 'vue';
import App from './App.vue';
import store from './store';
new Vue({
render: h => h(App),
store
}).$mount('#app');
Подключаем первую зависимость
Теперь мы хотим подключить в наш проект axios и создать для него некую конфигурацию. Я придерживался терминологии Nuxt и создал в src
каталог plugins
. Внутри каталога – файлы index.js
и axios.js
.
src
- plugins
-- index.js
-- axios.js
- store
- App.vue
- main.js
Как было сказано выше, каждый плагин должен экспортировать функцию. При этом внутри функции мы хотим иметь доступ к store и впоследствии – функцию inject
.
axios.js
import axios from 'axios';
export default function (app) {
// можем задать здесь любую конфигурацию плагина – заголовки, авторизацию, interceptors и т.п.
axios.defaults.baseURL = process.env.API_BASE_URL;
axios.defaults.headers.common['Accept'] = 'application/json';
axios.defaults.headers.post['Content-Type'] = 'application/json';
axios.interceptors.request.use(config => {
...
return config;
});
}
index.js
:
import Vue from 'vue';
import axios from './axios';
export default function (app) {
let inject = () => {}; // объявляем функцию inject, позже мы добавим в нее код для Dependency Injection
axios(app, inject); // передаем в наш плагин будущий экземпляр Vue и созданную функцию
}
Как можно заметить, файл index.js
тоже экспортирует функцию. Это сделано для того, чтобы иметь возможность передать туда объект app
. Теперь немного поменяем main.js
и вызовем эту функцию.
main.js
:
import Vue from 'vue';
import App from './App.vue';
import store from './store';
import initPlugins from './plugins'; // импортируем новую функцию
// объект, который передается конструктору Vue, объявляем отдельно, чтобы передать его функции initPlugins
const app = {
render: h => h(App),
store
};
initPlugins(app);
new Vue(app).$mount('#app'); // измененный функцией initPlugins объект передаем конструктору
Результат
На данном этапе мы добились того, что убрали конфигурацию плагина из main.js
в отдельный файл.
Кстати, польза от передачи объекта app
всем нашим плагинам в том, что внутри каждого плагина у нас теперь есть доступ к store. Можно свободно использовать его, вызывая commit
, dispatch
, а также обращаясь к store.state
и store.getters
.
Если вы любите ES6-style, можете даже сделать так:
axios.js
import axios from 'axios';
export default function ({store: {dispatch, commit, state, getters}}) {
...
}
Второй этап – Dependency Injection
Мы уже создали первый плагин и сейчас наш проект выглядит так:
src
- plugins
-- index.js
-- axios.js
- store
- App.vue
- main.js
Так как в большинстве библиотек, где это действительно необходимо, Dependency Injection уже реализована за счет Vue.use
, то мы создадим свой собственный простой плагин.
Например, попробуем повторить то, что делает vue-wait
. Это достаточно тяжелая библиотека, поэтому если вы хотите показать лоадер на паре кнопок, лучше от нее отказаться. Однако я не смог устоять перед ее удобством и повторил в своем проекте ее базовый функционал, включая синтаксический сахар в store.
Wait Plugin
Создадим в каталоге plugins
еще один файл – wait.js
.
У меня уже есть vuex-модуль, который я также назвал wait
. Он делает три простых действия:
— start
— устанавливает в state свойство объекта с именем action
в true
— end
— удаляет из state свойство объекта с именем action
— is
— получает из state свойство объекта с именем action
В этом плагине мы будем его использовать.
wait.js
export default function ({store: {dispatch, getters}}, inject) {
const wait = {
start: action => dispatch('wait/start', action),
end: action => dispatch('wait/end', action),
is: action => getters['wait/waiting'](action)
};
inject('wait', wait);
}
И подключаем наш плагин:
index.js
:
import Vue from 'vue';
import axios from './axios';
import wait from './wait';
export default function (app) {
let inject = () => {}; Injection
axios(app, inject);
wait(app, inject);
}
Функция inject
Теперь реализуем функцию inject
.
// функция принимает 2 параметра:
// name – имя, по которому плагин будет доступен в this. Обратите внимание, что во Vue принято использовать имя с префиксом доллар для Dependency Injection
// plugin – непосредственно, что будет доступно по имени в this. Как правило, это объект, но может быть также любой другой тип данных или функция
let inject = (name, plugin) => {
let key = `$${name}`; // добавляем доллар к имени свойства
app[key] = plugin; // кладем свойство в объект app
app.store[key] = plugin; // кладем свойство в объект store
// магия Vue.prototype
Vue.use(() => {
if (Vue.prototype.hasOwnProperty(key)) {
return;
}
Object.defineProperty(Vue.prototype, key, {
get () {
return this.$root.$options[key];
}
});
});
};
Магия Vue.prototype
Теперь о магии. В документации Vue сказано, что достаточно написать Vue.prototype.$appName = 'Моё приложение';
и $appName
станет доступно в this
.
Однако на деле оказалось, что это не так. Вследствие гуглинга не нашлось ответа, почему такая конструкция не заработала. Поэтому я решил обратиться к авторам плагина, которые уже это реализовали.
Глобальный mixin
Как и в нашем примере, я посмотрел код плагина vue-wait
. Они предлагают такую реализацию (исходный код очищен для наглядности):
Vue.mixin({
beforeCreate() {
const { wait, store } = this.$options;
let instance = null;
instance.init(Vue, store); // inject to store
this.$wait = instance; // inject to app
}
});
Вместо прототипа предлагается использовать глобальный mixin. Эффект в общем-то тот же, возможно, за исключением каких-то нюансов. Но учитывая, что и в store inject делается здесь же, выглядит не совсем right way и совсем не соответствует описанному в документации.
А если все же prototype?
Идея решения с прототипом, которая используется в коде функции inject
была позаимствована у Nuxt. Выглядит она намного более right way, чем глобальный mixin, поэтому я остановился на ней.
Vue.use(() => {
// проверяем, что такого свойства еще нет в прототипе
if (Vue.prototype.hasOwnProperty(key)) {
return;
}
// определяем новое свойство прототипа, взяв его значение из ранее добавленной в объект app переменной
Object.defineProperty(Vue.prototype, key, {
get () {
return this.$root.$options[key]; // геттер нужен, чтобы использовать контекст this
}
});
});
Результат
После этих манипуляций мы получаем возможность обратиться к this.$wait
из любого компонента, а также любого метода в store.
Что получилось
Структура проекта:
src
- plugins
-- index.js
-- axios.js
-- wait.js
- store
- App.vue
- main.js
index.js
:
import Vue from 'vue';
import axios from './axios';
import wait from './wait';
export default function (app) {
let inject = (name, plugin) => {
let key = `$${name}`;
app[key] = plugin;
app.store[key] = plugin;
Vue.use(() => {
if (Vue.prototype.hasOwnProperty(key)) {
return;
}
Object.defineProperty(Vue.prototype, key, {
get () {
return this.$root.$options[key];
}
});
});
};
axios(app, inject);
wait(app, inject);
}
wait.js
export default function ({store: {dispatch, getters}}, inject) {
const wait = {
start: action => dispatch('wait/start', action),
end: action => dispatch('wait/end', action),
is: action => getters['wait/waiting'](action)
};
inject('wait', wait);
}
axios.js
import axios from 'axios';
export default function (app) {
axios.defaults.baseURL = process.env.API_BASE_URL;
axios.defaults.headers.common['Accept'] = 'application/json';
axios.defaults.headers.post['Content-Type'] = 'application/json';
}
main.js
:
import Vue from 'vue';
import App from './App.vue';
import store from './store';
import initPlugins from './plugins';
const app = {
render: h => h(App),
store
};
initPlugins(app);
new Vue(app).$mount('#app');
Заключение
В результате проведенных манипуляций мы получили один импорт и один вызов функции в файле main.js
. А также теперь сразу понятно, где искать конфиг для каждого плагина и каждую глобальную зависимость.
При добавлении нового плагина нужно всего лишь создать файл, который экспортирует функцию, импортировать его в index.js
и вызвать эту функцию.
В моей практике такая структура показала себя очень удобной, к тому же она легко переносится из проекта в проект. Теперь нет никакой боли, если нужно сделать Dependency Injection или сконфигурировать очередной плагин.
Делитесь своим опытом организации зависимостей в комментариях. Успешных проектов!
Автор: 2developers