Как организовать ваши зависимости во Vue-приложении

от автора

Все, кто знаком с 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 или сконфигурировать очередной плагин.

Делитесь своим опытом организации зависимостей в комментариях. Успешных проектов!

Автор статьи:
Дина Абрамова
Frontend Developer


ссылка на оригинал статьи https://habr.com/post/423013/


Комментарии

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

Ваш адрес email не будет опубликован. Обязательные поля помечены *