Very large vue applications

Cover image

So you wanna build a large app?

Building an application is a lot of work and following through takes some great will power. Making it easier to build can be a good way to ensure your project gets finished and who doesn't like over engineering stuff?
In this post we will talk about setting up a vue application with quasar (quasar not required) that will be easier to manage.

Why would i want to do this?

  • It makes sharing a code base with your team easier
  • It helps reinforce naming conventions
  • You can get paid big bucks for knowing this kind of stuff
  • Better developer experience for larger applications
  • Less cognitive overhead

Ok, you've convinced me

This is all well and great... But how?
Hold up there cowboy... You need a project that runs webpack, similar to what vue cli or quasar cli creates. But do your self a favor and use quasar. You'll thank me later.

Quick setup with quasar

Lets do a super quick quasar intro.
If your not using quasar feel free to skip this section.
Break open the terminal and run:
npm i -g @quasar/cli
quasar create my-large-app
You will get a bunch of options, the only one that we really care about in this post is Vuex. Make sure it gets installed, the others are up to you.
cd my-large-app
code .
quasar dev

Next steps

Now that you have a running vue project with vuex and a router we can move on to the secret sauce. The directory structure is the biggest player in this post. Here is how i have set projects up in the past.

.
└── src
    ├── modules
    │   ├── layouts
    │   │   ├── default.vue
    │   │   └── routes.vue
    │   ├── blog-list
    │   │   ├── store
    │   │   │   └── index.js
    │   │   ├── index.vue
    │   │   └── routes.vue
    │   └── blog-edit
    │       ├── index.vue
    │       └── routes.vue
    ├── store
    │   ├── store-generator.js
    │   └── index.js
    └── router
        ├── build-route-tree.js
        ├── route-generator.js
        └── index.js

Something to note about these files and folders is i made the switch to all param-cased. its a personal preference but it has paid off in my case.

The first thing to do is get the router working so we can at least see the pages.
We need to import all the routes that we create in each module. So lets look at a "sane" way to do that.

There is quite a lot to unpack here so lets go step by step.

First lets look at the anatomy of a routes.js file.

src/modules/layouts/routes.js - root level routes
  // i opted to put the name of a route with no path in the meta
  // because vue-router does not like when pathless routes have names
  export default () => [ 
    {
      path: '',
      meta: { name: 'main' }
      components: () => import('./index.vue')
    }
  ]
src/modules/blog-list/routes.js - child level routes
  export default () => [ 
    {
      // this references the name of the parent route
      // in our case the layout with the meta name
      parent: 'main', 
      routes: [
        { 
          name: 'blog-list',
          path: 'blog',
          components: () => import('./index.vue')
        }
      ]
    }
  ]
  // you can optionally return an object instead of an array of objects

Ok, simple enough but how can we make vue-router read our custom rout nonsense.
I really wanted all the files for a component to live in the same folder so i created some supporting code to take care of importing and building all the routes for me.

src/router/route-generator.js
// cast array takes anything its passed and ensures its in an array
import castArray from 'lodash/castArray'
import { buildRouteTree } from './build-route-tree'


export default async () => {
  // scan recursively the src/modules folder for any files that
  // match routes.js or ANYTHING.routes.js and return any matches
  const context = require.context('src/modules/', true, /.*routes\.js$/)
  let routes = []

  // get the actual string path EG: src/modules/blog-list/routes.js
  const importedRoutes = context.keys()
  for (const index in importedRoutes) {

// iterate over the object of routes and import each one
const route = (
  await import(`src/modules${importedRoutes[index].substr(1)}`)
).default

// i have deemed to make these roues return a function that 
// can optionally be a promise that way we can grab roues on the fly
// either from a server or elsewhere if needed
const resolvedRoutes = await Promise.resolve(route())

// we are making sure the route returned is a list of routes 
const routesArr = castArray(resolvedRoutes)

// if the routes have a parent EG: are not root level we concat them
if (routesArr[0] && routesArr[0].parent) {

  // this allows us to return an array of routes that way
  // we can reuse a component at a different route/parent
  routes = routes.concat(routesArr)
} else {

// else we push the array in to the routes array cuz its root level
  routes.push(resolvedRoutes)
}
  }

  // so routes ends up containing an array of objects/arrays [ {}, [] ].
  // now we can pass these resolved routes to our builder
  // to turn in to a vue-router accepted object/tree
  const routeTree = buildRouteTree(routes)
  return Promise.all(routeTree)
}

Ahh the fruits of a mad man..
This next collection of functions will take the flat array and recursively construct the flat array in to a tree that the vue router will accept.

src/tools/build-route-tree
import get from 'lodash/get'
import set from 'lodash/set'

// This function will recursively search for a name through a valid
// Vue-router object and return a path to that
// route in the shape of an array ['children' ,1, 'children', 0]
function searchRoutes (name, routes) {
  const path = []
  let found = false
  function find (name, routes) {
let i = routes.length
while (i--) {
  const route = routes[i]
  path.push(i)
  if (route.children && route.children.length) {
    find(name, route.children, path)
  }
  if ( (route.name && route.name === name) || (route.meta && route.meta.name === name) || found ) {
    found = true
    return path
  }
  path.pop()
}
return path
  }
  find(name, routes)
  return path
}

// this function takes our flat routes
//and turns em in to a valid Vue-router object/tree
export const buildRouteTree = (routes) => {
  let routeTree = []
  let i = routes.length
  let lastRouteLength = routes.length

  // while (i--) is really fast
  while (i--) {
const route = routes[i]

// If the route is an array we should put these items at the root level
if (Array.isArray(route)) {
  routeTree = [...routeTree, ...route]
  routes.splice(i, 1)
}

// If its not a root level route do some matching and appending
if (route.parent) {
  // find where the item goes in the tree
  const path = searchRoutes(route.parent, routeTree)

  // if it has a home we use some poor programming skills to
  // concat the matched route's children and out new routes
  if (path.length) {
    const theseRoutes = get(routeTree, [...path.join('|children|').split('|'), 'children']) || []
    set(routeTree, [...path.join('|children|').split('|'), 'children'], [...theseRoutes, ...route.routes])
    routes.splice(i, 1)
  }
}

// if we have routes left because the search failed
// due to the parent not existing in the tree yet we try again
// if there are no routes left to parse the loop will exit
if (i === 0 && routes.length > 0) {

  // if there was 2 iterations and we failed to find a parent
  // that means there is an orphan route we need to break out of the loop
  // because our routes.length will never be 0 in this case
  if (lastRouteLength === routes.length) break // No infinite loop
  i = routes.length
  lastRouteLength = routes.length
}
  }

  // Then a little house keeping to tell me if there are orphan routes
  if (routes.length) console.warn(`${routes.length} route(s) failed to build`, JSON.stringify(routes, null, 2))
  return routeTree
}

now that we have that routs object its a simple matter of plugging that in to the vue router.

And there you have it.. Part 1 of our descent into madness.
I will cover a similar strategy for importing vue stores in the next part of this series

Summary

Make large projects easier on your self by organizing them in to a module structure.
This creates a new problem. We must collect all the routes and assemble them into a valid vue-router object/tree. To solve this problem we created a recursive route builder that will generate a valid vue-router object/tree for us. We should now have a valid routes array that we can plug in to our vue router.

TLDR

Large projects are hard, organize them well, profit.