📜 ⬆️ ⬇️

How to organize your dependencies in a Vue application

Anyone familiar with Vue knows that a Vue application has one entry point - the main.js file. 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, plugins). 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 make 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 in the root instance of Vue and in the store object. 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 the fact that main.js “lose weight” significantly, you will also be able to use dependencies anywhere in the application without any extra imports.

A striking example of Dependency Injection is the vue-router . It is used not so often - get the parameters of the current route, 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 the $wait property not only to the Vue instance, but also to the vuex store. Given the specifics of the plugin, it 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 calling $wait.start('action') and $wait.end('action') before and after each call to action, you can simply call these methods once in the action itself. And this 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:
src
- store
- App.vue
- main.js

main.js looks like this:
 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 followed the Nuxt terminology and created a plugins directory in src . Inside the directory are the index.js and axios.js .

src
- plugins
-- index.js
-- axios.js
- store
- App.vue
- main.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 inject function.

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    } 


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

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    


Result


At this stage, we achieved that we removed the plugin configuration from main.js to a separate file.

By the way, the benefit of passing the app object to all our plugins is that within each plug-in we now have access to the store. You can freely use it by calling commit , dispatch , as well as by accessing store.state and store.getters .

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

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

The second stage - Dependency Injection


We have already created the first plugin and now our project looks like this:

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

Since in most libraries where this is really necessary, Dependency Injection is already implemented by Vue.use , we will create our own simple plugin.

For example, try to repeat what vue-wait 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.

Wait plugin


Create another file in the plugins directory - wait.js

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

- start - sets the state of the object with the name action to true
- end - removes from the state the property of the object with the name action
- is - gets from state a property of the object named action

In this plugin we will use it.

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); } 


And connect our plugin:

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 function


Now let's implement the inject function.
 //   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]; } }); }); }; 


Magic Vue.prototype


Now about magic. The Vue documentation says that it’s enough to write Vue.prototype.$appName = ' '; and $appName will be 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 code for the vue-wait plugin. 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 store this.$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 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, which is used in the code of the inject function inject was 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 () { return this.$root.$options[key]; //  ,    this } }); }); 


Result


After these manipulations, we are able to access this.$wait from 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'; 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'); 

Conclusion


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

When adding a new plug-in, you just need to create a file that exports the function, import it into index.js and 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 in organizing dependencies in the comments. Successful projects!

Source: https://habr.com/ru/post/423013/


All Articles