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 или сконфигурировать очередной плагин.
Делитесь своим опытом организации зависимостей в комментариях. Успешных проектов!
Комментарии (11)
pmcode
23.09.2018 08:18+1Имхо, слишком запутанная схема. Вы передаете экземпляр
app
в плагин, где он так вообще ни разу и не используется, и туда же передаетеinject()
только для того, чтобы плагин зарегистрировал себя сам. По мне, что такое было бы намного читабельнее и очевиднее:
registerPrototype('axios', axios); registerPrototype('wait', wait);
Плюс, такой способ позволил бы возвращать инстанс плагина из функции, чтобы его можно было импортировать вручную. Доступ через глобальный объект удобен изнутри компонента, а в слое сервисов я бы предпочел явным образом импортированные зависимости.
b360124
24.09.2018 13:201) В модуле axios.js, не дописан inject
inject('http', axios);
2) Почему плагин не добавить канонически, как по доках
Вот как у меня все получилось
plugins/index.js
import Vue from 'vue';
import axios from './axios';
import wait from './wait';
export default function (app) {
let inject = (plugin) => {
Object.assign(app.store, plugin);
Vue.use({install(Vue) { Object.assign(Vue.prototype, plugin)}});
}
axios(app, inject);
wait(app, inject);
}
wait.js
export default function ({store: {dispatch, getters}}, inject) {
const $wait = {
...
};
inject( { $wait } );
}2developers Автор
24.09.2018 14:401) Конкретно в моем проекте нет необходимости делать inject для axios. Вызовы API я храню отдельно, в компонентах и сторе
this.$axios
я не использую. В статье пример с axios был с целью показать конфигурирование библиотек внутри плагинов, а не Dependency Injection в корневой экземпляр Vue.
2) Такой способ из документации у меня не сработал, это упомянуто в статье.
Vue.prototype.$myMethod = function (methodOptions) { // некоторая логика ... }
ДелатьObject.assign
в функцииinstall
вместо простого присваивания не пробовал.b360124
24.09.2018 19:33ну так там же не просто присваивание прототипу нового метода, а там идет через создание объекта с методом install, уже в котором идет присваивание прототипа, который уже подключается через
Vue.use(...)
Это реально рабочий вариант, не только у меня. Попробуйте, таким способом сделать и тогда заработает.
justboris
А в чем смысл навешивания разнообразных плагинов на центральный объект? Можно же просто импортировать библиотеки по месту использования – сразу будет понятно что откуда тянется.
Мне кажется от миксинов в прототипы отказались сразу после jQuery, с его системой плагинов через jQuery.fn.functionName
2developers Автор
Смысл такой же, как и в использовании глобальных миксинов и компонентов. Чтобы не делать импорт во всех файлах, если библиотека используется часто.
Понятно, что применять подобные зависимости надо обдуманно, чтобы не засорять глобальный экземпляр Vue.
justboris
Отсутствие лишний писанины это хороший аргумент.
Но с другой стороны, без явных прописанных импортов будет сложно отследить, где используется какая-то библиотека. А это нужно, когда проводится апгрейд версии, или чтобы удалить библиотеку после рефакторинга.
PaulMaly
Мне кажется зависит от того должна ли бы зависимость синглтоном с общим для приложения стейтом (например router) или же это просто набор утилит.
justboris
Все верно. Для центровых штук, вроде vue-router или vuex это уместно. А вот простым утилитам эта магия ни к чему.
PaulMaly
Все так.