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 или сконфигурировать очередной плагин.
Делитесь своим опытом организации зависимостей в комментариях. Успешных проектов!
Автор статьи:
Дина Абрамова
Frontend Developer
ссылка на оригинал статьи https://habr.com/post/423013/

Добавить комментарий