How to organize your dependencies in a Vue application

    Anyone familiar with Vue knows that a Vue application has one entry point - a file main.js. There, in addition to creating an instance of Vue, there is an import and a kind of Dependency Injection of all your global dependencies (directives, components, plug-ins). The larger the project, the more dependencies become, which, moreover, each have their own configuration. As a result, we get one huge file with all configurations.
    This article will discuss how to organize global dependencies to avoid this.



    Why write it yourself?


    Many may think - why is it necessary, if there is, for example, Nuxt , which will do it for you? I used it in my projects too, but in simple projects this may be redundant. In addition, no one has canceled projects with legacy-code that fall on you like snow on your head. And to connect the framework there - practically to do it from scratch.

    Inspirer


    The inspirer of such an organization was Nuxt. It was used by me on a large project with Vue.
    Nuxt has a great feature - plugins. Each plugin is a file that exports a function. The config is passed to the function, which will also be passed to the Vue constructor when creating the instance, as well as the entire store .

    In addition, an extremely useful feature is available in each plugin inject. It does Dependency Injection to the root instance of Vue and to the object store. And this means that in each component, in each storage function, the specified dependency will be available through this.

    Where can it come in handy?


    In addition to being main.jssignificantly “thinner,” you will also be able to use dependency anywhere in the application without unnecessary imports.

    A striking example of Dependency Injection is the vue-router . It is used not so often - to get the parameters of the current route, to make a redirect, but this is a global dependence. If it can be useful in any component, then why not make it global? In addition, thanks to this, its state will also be stored globally and change for the entire application.

    Another example is vue-wait . The developers of this plugin went further and added a property$waitnot only in the Vue instance, but also in the vuex store. Given the specifics of the plugin, this is extremely useful. For example, the store has an action that is called in several components. And in each case, you need to show the loader on some element. Instead of before and after each call to action cause $wait.start('action')and $wait.end('action')you can simply call these methods once the action. And it is much more readable and less verbose than dispatch('wait/start', 'action' {root: true}). In the case of the store is syntactic sugar.

    From words to code


    Basic project structure


    Let's see how the project looks now: looks like this:
    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');
    


    We connect the first dependency


    Now we want to connect axios to our project and create a configuration for it. I stuck to Nuxt terminology and created a srcdirectory plugins. Inside the directory - files index.jsand axios.js. As mentioned above, each plugin must export a function. At the same time inside the function we want to have access to the store and subsequently - the function .

    src
    - plugins
    -- index.js
    -- axios.js
    - store
    - App.vue
    - main.js

    inject

    axios.js
    import axios from'axios';
    exportdefaultfunction (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';
    exportdefaultfunction (app) {
      let inject = () => {}; // объявляем функцию inject, позже мы добавим в нее код для Dependency Injection
      axios(app, inject); // передаем в наш плагин будущий экземпляр Vue и созданную функцию
    }
    


    As you can see, the file index.jsalso exports the function. This is done in order to be able to pass an object there app. Now let's change a bit main.jsand call this function.

    main.js:
    import Vue from'vue';
    import App from'./App.vue';
    import store from'./store';
    import initPlugins from'./plugins'; // импортируем новую функцию// объект, который передается конструктору Vue, объявляем отдельно, чтобы передать его функции initPluginsconst app = {
      render: h => h(App),
      store
    };
    initPlugins(app); 
    new Vue(app).$mount('#app'); // измененный функцией initPlugins объект передаем конструктору


    Result


    At this stage, we achieved that we removed the configuration of the plug-in from main.jsa separate file.

    By the way, the benefit of transferring the object to appall our plugins is that within each plug-in we now have access to the store. You can use it freely, causing commit, dispatchas well as addressing store.stateand store.getters.

    If you like ES6-style, you can even do this:

    axios.js
    import axios from'axios';
    exportdefaultfunction ({store: {dispatch, commit, state, getters}}) {
      ...
    }
    

    The second stage - Dependency Injection


    We have already created the first plugin and now our project looks like this: Since in most libraries where this is really necessary, Dependency Injection has already been implemented , we will create our own simple plugin. For example, try to repeat what it does . This is quite a heavy library, so if you want to show a loader on a pair of buttons, it is better to abandon it. However, I could not resist its convenience and repeated in my project its basic functionality, including syntactic sugar in the store.

    src
    - plugins
    -- index.js
    -- axios.js
    - store
    - App.vue
    - main.js

    Vue.use

    vue-wait

    Wait plugin


    Create pluginsanother file in the directory - wait.js.

    I already have a vuex module, which I also called wait. He does three simple steps:

    - start- sets the state property of the object named actionin the true
    - end- removes from the state property of the object with the name action
    - is- receives from the state property of the object with the name of action

    this plugin we use it.

    wait.js
    exportdefaultfunction ({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);
    }
    


    And connect our plugin

    index.js::
    import Vue from'vue';
    import axios from'./axios';
    import wait from'./wait';
    exportdefaultfunction (app) {
      let inject = () => {};  Injection
      axios(app, inject);
      wait(app, inject);
    }
    


    Inject function


    Now we implement the function 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 () {
              returnthis.$root.$options[key];
            }
          });
        });
      };
    


    Magic Vue.prototype


    Now about magic. The Vue documentation says that it is enough to write Vue.prototype.$appName = 'Моё приложение';and $appNamebecome available in this.

    However, in fact, it turned out that it is not. As a result of googling, there was no answer why such a construction did not work. Therefore, I decided to contact the authors of the plugin who have already implemented it.

    Global mixin


    As in our example, I looked at the plugin code vue-wait. They offer this implementation (the source code is cleaned for clarity):

    Vue.mixin({
        beforeCreate() {
          const { wait, store } = this.$options;
          let instance = null;
          instance.init(Vue, store); // inject to storethis.$wait = instance; // inject to app
        }
      });
    

    Instead of the prototype, it is proposed to use the global mixin. The effect is basically the same, perhaps with the exception of some nuances. But considering that the store inject is being done here, it does not look exactly the right way and does not at all correspond to the one described in the documentation.

    And if still prototype?


    The idea of ​​the solution with the prototype that is used in the function code injectwas borrowed from Nuxt. It looks a lot more right way than the global mixin, so I settled on it.

        Vue.use(() => {
          // проверяем, что такого свойства еще нет в прототипеif (Vue.prototype.hasOwnProperty(key)) {
            return;
          }
          // определяем новое свойство прототипа, взяв его значение из ранее добавленной в объект app переменнойObject.defineProperty(Vue.prototype, key, {
            get () {
              returnthis.$root.$options[key]; // геттер нужен, чтобы использовать контекст this
            }
          });
        });
    


    Result


    After these manipulations, we are able to access this.$waitfrom any component, as well as any method in the store.

    What happened


    Project structure ::

    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';
    exportdefaultfunction (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 () {
              returnthis.$root.$options[key];
            }
          });
        });
      };
      axios(app, inject);
      wait(app, inject);
    }
    


    wait.js
    exportdefaultfunction ({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';
    exportdefaultfunction (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');
    

    Conclusion


    As a result of the manipulations, we received one import and one function call in the file main.js. And now it’s immediately clear where to find the config for each plug-in and every global dependency.

    When you add a new plug-in, you just need to create a file that exports the function, import it into index.jsand call this function.

    In my practice, such a structure proved to be very convenient, moreover, it is easily transferred from the project to the project. Now there is no pain, if you need to do Dependency Injection or configure another plugin.

    Share your experience with dependency management in comments. Successful projects!

    Also popular now: