Make the Router a Vue.js Plugin

Part 4 of 4 in our Creating Your Own Router series.
Written by Alex Jover Morales
Make the Router a Vue.js Plugin

So far we’ve build the router in a component and a history module within the source code of our app. The problem of it is that the router is tied to the app, specially because the routes are defined within the AppRouter component.

That could be improved by moving out the routes from the component and passing them as a parameter. But still, the developer must import the AppRouter component and the history.js module and use it around.

We could improve and simplify it by creating a Vue.js plugin, which will make the router 100% reusable between apps and will allow to make it easier to use and customizable for the final developer.

Creating a Vue.js plugin is not something that we need do from scratch: we can create it by using the components and modules that we already have. That means the process to create a plugin could be progressive, you can delay it until you need it. In our case, we have the AppRouter.vue component and the history.js module already in place.

Let’s see how we can use them to create a Vue.js plugin for the Router.

Creating the Router Plugin

From now on, consider the router to be an external package that we could install from npm that “just works”. But for now, in order to separate the plugin from the rest of the application, let’s better create a folder called plugin where we move the AppRouter.vue component.

The Vue.js plugin structure should be an object with an install(Vue, options) function signature. This install function is called when we register the plugin, by using Vue.use(AnyPlugin).

The install function takes two parameters:

  • **Vue**: the global Vue object that we can manipulate according to our needs.
  • **options**: the options passed as the second argument to the Vue.use function call.

Let’s then create a plugin/index.js file as the entry point for the router plugin:

// plugin/index.js
export default {
  install(Vue, options) {}
};

Within the install function of a Vue.js plugin, we can do several things on the global scope:

  • Register global components, directives, transitions, etc.
  • Add a global mixin in order to add component options
  • Add static methods or properties to the Vue class
  • Add instance methods or properties to Vue.prototype

The first thing we can do is to register the AppRouter.vue component. In that way, we can remove it from App.vue but still use it as <app-router></app-router> since it’s now registered globally:

// plugin/index.js
import AppRouter from "./AppRouter";
export default {
  install(Vue, options) {
    Vue.component("AppRouter", AppRouter);
  }
};

Extending the Vue instance

Remember we have a history.js module as well? Its logic is tied to the router and it would be nice if the final user doesn’t know about its existence. So start by moving the history.js module to the plugin folder.

But now we have a problem: the history.js module is used in AppRouter.vue as well as in App.vue. Thus, is not enough just to move it, since App.vue would still need access to it.
To solve that, we can add a $pushRoute instance method on the install function:

// plugin/index.js
import AppRouter from "./AppRouter";
import { push } from "./history";
export default {
  install(Vue, options) {
    Vue.component("AppRouter", AppRouter);

    // Set $pushRoute to the push function
    Vue.prototype.$pushRoute = push;
  }
};

Have you noticed the dollar *$* prefix? That’s a convention taken in the Vue.js community to define instance methods and properties. You can find it on the Vue.js style guide as well

By adding the $pushRoute method we removed the dependency between App.vue and history.js.

Master Vue.js with Vue School

Top notch Vue.js courses and over 200 lessons for just $12 per month!

Because of having the AppRouter component registered globally and the $pushRoute method added to the Vue instance, we can go to App.vue and remove the imports, component and pushreferences, cleaning up its dependencies:

<!-- App.vue -->
<template>
  <div>
    <button @click="goTo('/articles')">Go to Articles</button>
    <button @click="goTo('/')">Go to Home</button>
    <app-router></app-router>
  </div>
</template>
<script>
export default {
  methods: {
    goTo(route) {
      this.$pushRoute(route);
    }
  }
};
</script>

We still need to somehow pass the routes to the router plugin, and there are several options to do so.

If you’re curious enough, you can check that in the official vue-router they use a beforeCreate mixin, accessing the router actions from the instance options:

Vue.mixin({
  beforeCreate() {
    this._router = this.$options.router;
    // ...
  }
});

If you do it in that way, you’d pass the router as an option to the root Vue.js instance:

const router = new Router({
  "/": SomeComponent
});

new Vue({
  el: "#app",
  router,
  render: h => h(App)
});

But here we’ll use a simpler approach: we can use the plugin options to pass the routes to the plugin and then we’ll store them in an instance property within the install function:

// plugin/index.js
import AppRouter from "./AppRouter";
import { push } from "./history";
export default {
  install(Vue, options) {
    Vue.component("AppRouter", AppRouter);
    // Add the routes to the $routes instance property
    Vue.prototype.$routes = options.routes;
    Vue.prototype.$pushRoute = push;
  }
};

Finally, update the AppRouter.vue computed property routedComponent so it gets the right route from the $routes instance property:

// AppRouter.vue
export default {
  computed: {
    routedComponent() {
      return this.$routes[this.current];
    }
  }
};

Using the plugin

The router plugin is ready to use and totally decoupled from the app, now is time to use it.
A Vue.js plugin needs to be invoked using Vue.use(Plugin), and our router plugin needs a routes options. Then, go to the root file of your app and update it accordingly:

// index.js
import Vue from "vue";
import App from "./src/App";
import AppRouter from "./plugin";
import HomePage from "./src/HomePage";
import ArticlesPage from "./src/ArticlesPage";

// Create routes objects
const routes = {
  "/": HomePage,
  "/articles": ArticlesPage
};

// Use the plugin
Vue.use(AppRouter, {
  routes
});

// ...

If you run the project, it should work exactly as it was working before.

Extra: Add a router hook

Having the router as a plugin gives us the flexibility to add more options and functionality to it. For example, it’s usual to have router hooks at different levels.

Those hooks are functions that are triggered at certain points when the route changes. For example, it could be before or after the route changes, allowing the user to perform different actions at those points.

For simplicity and since we only have one listeners variable available in history.js module, add just a global hook after the route change. Let’s use a hook router option to define a hook that just logs from where it comes and where it goes when a route change happens:

// index.js
// ...
Vue.use(AppRouter, {
  routes,
  hook: (to, from) => console.log(to, from)
});
// ...

Remember the listener callbacks receive a to and from parameters, that’s why we have access to them. Then, from the install function of the router plugin, we can use the listen function from the history.js module to be notified of the route changes:

import AppRouter from "./AppRouter";
import { listen, push } from "./history";
export default {
  install(Vue, options) {
    Vue.component("AppRouter", AppRouter);
    Vue.prototype.$routes = options.routes;
    Vue.prototype.$pushRoute = push;

    // Add a listener if the hook option is provided
    if (options.hook) {
      listen(options.hook);
    }
  }
};

Remember to check for the existence of the options.hook property to prevent it from crashing.

Wrapping up

We’ve made a quite simple yet complete router plugin from scratch. If you’ve followed along, I’m sure you understand the basics of a router and how they work under the hood.

I hope it has opened up your mind as well, so you can add more features to the router we’ve created, such as:

  • Implement guards
  • Allow hash mode
  • Provide a <link> global component to declaratively navigate through routes

You can find here the code and demo from this article.

Master Vue.js with Vue School

Top notch Vue.js courses and over 200 lessons for just $12 per month!


Article written by Alex Jover Morales

Passionate web developer. Author of Test Vue.js components with Jest on Leanpub. I co-organize Alicante Frontend. Interested in web performance, PWA, the human side of code and wellness. Cat lover, sports practitioner and good friend of his friends. His bike goes with him.