A better way of routing in a Vue project
20/09/2018 09:00
4 min. to read

The motivation

The goal here is quite simple, I want to show a way to set up Vue's router without having a huge file containing all your routes. Well, if you already worked on a large project using Vue, you may have faced this problem, you probably have this file importing the router and setting up all the relations between a user route and a component in your app.

If you think you going to end up with for example twenty routes or if you think having routes in separated files is a better idea, this article is for you.

The idea

Let's imagine a huge app with 50 routes, and your job is to pick each route and link it to an existing component in your app and set this up to the router. The router expects an array where each element will have at least three properties: path, name, and component. What you are going to do inside your file - router/index.js, for example - is the following:

  • Import all the components to each route
  • Initialize the array of routes
  • Setup everything to the router

Yes, you will probably end up with more than one hundred lines of code for this.

Besides that, the size and the mess inside this single file, you have to consider the fact that this file is quite far from the components he is importing, and this doesn't feel right, does it?

Well, that said, my proposal is: What if you could write and load your routes in a file located in the same directory of the components you have to link?

How to do it

Let's imagine a simple app navigation menu represented on something like this:

[
  {
    name: "Menu 1",
    submenu: [
      {
        aggregator: "Menu 1",
        name: "Submenu 1.1",
        path: "/menu1/submenu11"
      },
      {
        aggregator: "Menu 1",
        name: "Submenu 1.2",
        path: "/menu1/submenu12"
      }
    ]
  }
  // ...
];

Knowing this format you can fetch this data and create a navigation menu component to display it nicely on your app.

What I want to show is how each path on this object relates to an entry of the router. The goal is to fulfill the router with everything he needs to know. Ok, let's think our app with the following folder structure to the templates.

📁 templates
  📁 product
    📄 ProductsList.vue
    📄 ProductsForm.vue
    📄 ProductsAdvancedSearch.vue
  📁 client
    📄 ClientList.vue
    📄 ClientForm.vue
    📄 ClientDashboard.vue
    📄 ClientCheckout.vue

As you probably realized, this can keep growing... Each SFC (Single File Component) here is a page to be displayed and registered with a route. The tricky part here is to have a file on each template's folder with the routes in an array to be exported. Our folder structure will become something like this:

📁 templates
  📁 product
    📄 _routes.js
    📄 ProductsList.vue
    📄 ProductsForm.vue
    📄 ProductsAdvancedSearch.vue
  📁 client
    📄 _routes.js
    📄 ClientList.vue
    📄 ClientForm.vue
    📄 ClientDashboard.vue
    📄 ClientCheckout.vue

This file called _routes.js will have an array of the following object:

  • path: Page's path.
  • name: Route name.
  • component: Component to be used.
  • meta: Any relevant information to the route.

    • menu: An object to the navigation menu.

      • aggregator: A parent menu, can be empty
      • nome: Text to be display at the navigation menu.

Inside the _routes.js file we will have something like this:

const ProductsList = () => import("./ProductsList");
const ProductsForm = () => import("./ProductsForm");
const ProductsAdvancedSearch = () => import("./ProductsAdvancedSearch");

export default [
  {
    path: "/products",
    name: "ProductsList",
    component: ProductsList,
    meta: {
      menu: {
        aggregator: "Products",
        name: "All products"
      }
    }
  },
  {
    path: "/products/form/{id}",
    name: "ProductsForm",
    component: ProductsForm,
    meta: {
      menu: {
        aggregator: "Product",
        name: "Product registration"
      }
    }
  },
  {
    path: "/products/search",
    name: "ProductsAdvancedSearch",
    component: ProductsAdvancedSearch,
    meta: {
      menu: {
        aggregator: "Product",
        name: "Find a product"
      }
    }
  }
];

So, you have to do the same to client and other domains you may have. After doing that, the main router file will become nothing more than a reader for all the pieces you put on your templates directory. He will have to:

  • Read all data from all _routes.js files,
  • Generate an array of routes to the router,
  • Generate another array to the navigation menu,
  • In the end, exports the router instance.

Will be something like this:

import Vue from "vue";
import Router from "vue-router";

const importAll = r => {
  return r.keys().map(r);
};
const importRoutes = importAll(require.context(".", true, /_routes.js$/));

// Generates an array with the routes
const routes = importRoutes.map(r => r.default).reduce((a, b) => a.concat(b));

// Generates another array with the navigation menu
const menu = routes
  .reduce(
    (a, b) =>
      b.meta
        ? a.concat({
            name: b.meta.menu.name,
            aggregator: b.meta.menu.aggregator,
            route: b.path
          })
        : a,
    []
  )
  .reduce((a, b) => {
    const menuItem = a.find(a => a.name === b.aggregator);
    if (menuItem) {
      menuItem.submenu.push(b);
    } else {
      a.push({ nome: b.aggregator, submenu: [b] });
    }
    return a;
  }, []);

// Uses the created router
Vue.use(Router);

// Register the navigation menu
Vue.prototype.$menu = menu;

export default new Router({
  mode: "history",
  base: process.env.BASE_URL,
  routes // All the routes from the begining
});

Well, about this code, I took heavy inspiration on the base components strategy, you can see it here, after reading this I came up with this idea. The require.context is a Webpack function that does all the magic to make this possible, besides that some map().reduce() was used to generate both arrays of routes and navigation menu.

Registering routes this way give us the opportunity to isolate even more the responsibility of the domain and is a way to avoid a lot of lines of code in a single file.