Code: Views-driven router paths, instead of a hardcoded mess — VueJS

As we started building a brand new web platform from scratch at ESGgen (www.esggen.com), I wanted to share a bit of this journey and how we approached certain things. I usually don’t write technical posts, but I decided to try something different. In the future, I might cover things like language packs, build process, release pipelines, branching strategy, release notes, versioning, and a lot more. And now, I’m starting with something relatively simple like better router paths configuration…

The problem

We will have hundreds of views and files in the /views/* folder, which is not uncommon. Yet, all supporting files and configurations don’t live anywhere near them. So, in a typical folder structure (which I’ve seen too many times and I don’t accept as a solution) we end up with things like router paths, store configuration, unit tests and assets usually living somewhere in the root instead of being together with a particular view they are related to. This is the first step to hell when the app grows fast and new teams are added to the project. Yet, we hope everybody will not end up in PR-conflict-hell, which can easily cripple the best software engineering team.

What we are trying to solve

  • Router configuration paths are defined in one massive file sitting in /router/index.js file, where all the magic happens.
  • Confusion and cognitive overhead come with editing and understanding complex files where anything can happen.
  • Shift responsibility for maintaining router paths from “everyone” to “a person working on a particular view.”
  • Split the router paths configuration into manageable chunks which live together with a view responsible for it.
  • Eliminate the need to update the app in many places when we decide to remove or add a new view.
  • Simplify PRs and create fewer opportunities for conflicts on code merges.

The solution

I looked around for different options, and people had various suggestions for tackling this problem. Still, I didn’t like any of them. In most cases, it required some kind of central configuration that will import these chunked router configs. In other cases, the solution was confusing and did not go along with my goals. So I decided to use my experience with building proper and complex build processes for front-end applications to solve it. It was easier than I expected.

  • Every view has its own /view/index.route file, which configures paths for this particular view.
  • View’s name dictates path names (doesn’t have to), making it a bit simpler to find and debug.
  • Keep the router configuration format close to the original one (simple object).
  • The built process should generate the final route configuration file based on all these chunked configs from each view.
  • It should be fast and create zero negative impact on a browser’s frontend performance.
  • Should support static paths (not dynamically loaded views) from the root router configuration (the template file).

This is part of my journey to create frictionless applications for users who will interact with the UI and software engineers who will spend their lives building and maintaining them. These simple solutions make a difference when a team grows to 50 engineers across 10 different delivery teams. We will all work on one repository, developing 4 applications simultaneously using shared Core. This is one of those things that is usually left behind. Non-engineers consider it unimportant, and it never gets done later because the Product team is asking for another feature to be delivered.

Step 1: Define router/index.js template

We know where the router configuration will live and how it will look (at least roughly), so we can define a blank template used later by the build process to generate the actual file.

I decided to create the template as index.js, and the final file is called index.built.js. The only reason is that it will be easier to clean it up after every build and add it to .gitignore. Of course, you can decide to do the other way around, whatever works for you.

/*******
* THIS IS A TEMPLATE FILE FOR THE BUILD PROCESS
* The below "marker" will be filled out every time the build process runs
* It is like a pre-processor which will find all *.route files and create dynamic router configuration
*******/
import { createRouter, createWebHistory } from 'vue-router'let routes = [
%ROUTES%
];
let router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
})
export default router

Step 2: Create your view/MyView/index.route file

Every view that has a path and can be reached in a browser has this file in the folder and includes all relevant router configurations. Since I use dynamically loaded views, I don’t deal with static imports and care about this option. It is possible to configure a static view dependency in the router template file only, which is enough for me (in case I need it).

As I said earlier, I wanted to keep the local index.route file as close as possible to the original router configuration format. So to be precise, it is the same, just a JavaScript object literal (kind of).

{
path: '/MyView',
name: 'MyView',
component: () => import(/* webpackChunkName: "MyView" */ '../views/MyView/index.vue')
},
{
path: '/MyView/SubView',
name: 'MyView-SubView',
component: () => import(/* webpackChunkName: "MyView-SubView" */ '../views/MyView/SubView/index.vue')
}

Step 3: Build process to output the final router/index.js configuration

I’m using nodeJS and custom Webpack (not a default VueJS CLI) to build the whole frontend, so I’ve got a lot of flexibility to run scripts.

Every build for dev, production or watch will require me to run a script that will grab all *.route files and generate the final router configuration. So I created a preprocessor.js script that will be run by the node, and it will go through the source folder, find all *.route files, merge the output and save to the newly created global router config file.

Yes, I made it fancy. I like clean comments in the console, and I really care about the performance, so I added some simple timers in case this becomes a bottleneck in the future due to a large number of files.

const helpers = require('./helpers.js');console.log('\nGENERATE VUEJS ROUTER ***************************');
console.time('* Total Time');
let allRoutes = helpers.findFiles('src', '.route');
helpers.saveResults('src/router/index.js', allRoutes, '%ROUTES%');
console.timeEnd('* Total Time');

Step 4: A few functions that will be required

As you can see, I extracted some extra methods into the helpers.js file as I will use them later to build other things like language packs.

I’m a big fan of JSDoc spec, so not a surprise that all my methods have proper descriptions following this spec. Thanks to that, IDE helps during coding with tips and highlights errors if I try to pass to a functions something that doesn’t match the expectations.

helpers.findFiles()

/**
* Recursive function to parse all subfolders from a parent, find specific files and return finalOutput for future parsing
* @param {string} startPath - root folder path
* @param {string} filter - an extension of files we looking for
* @param {string} finalOutput - object to be updated will parsed files we found
*/
exports.findFiles = function(startPath, filter, finalOutput=''){
// error handling, just in case
if (!fs.existsSync(startPath)){
console.log("\x1b[31mn* Directory doesn't exist. ",startPath);
return;
}
// do the job
var files=fs.readdirSync(startPath);
for(var i=0;i<files.length;i++){
var filename=path.join(startPath,files[i]);
var stat = fs.lstatSync(filename);
if (stat.isDirectory()){
finalOutput = findFiles(filename, filter, finalOutput); //recurse
}
else if (filename.indexOf(filter)>=0) {
console.log('*',filename);
let fileContent = fs.readFileSync(filename, 'utf8');// just in case, check for comma at the end as it shouldn't be there.
if (fileContent.slice(-1) === '}' || fileContent.slice(-1) === ']' ) {
fileContent += ',\n'
}
finalOutput += fileContent
};
};
return finalOutput
};
exports { findFiles }

A few improvements could be made in this file, but I assume devs are smart enough not to make simple mistakes in the first place. However, if they do, and it happens often, there is some validation we can do to make sure file formatting is correct.

helpers.saveResults()

/**
* It will take the given file and update the placeholder
* @param {string} destinationFile - the template file
* @param {string} content - what we want to write in the file
* @param {string} placeholder - placeholder name
* @param {string} [suffix] - optional extra suffix
*/
exports.saveResults = function (destinationFile, content, placeholder, suffix) {
if (suffix) { suffix+='.'}
else { suffix = ''}
let extension = destinationFile.split('.').pop();
let generatedFilename = destinationFile.replace(extension, suffix+PROCESSED_SUFFIX+extension);
fs.copyFile(destinationFile, generatedFilename, (err) => {
if (err) throw err;
let fileContent = fs.readFileSync(generatedFilename, 'utf8');
let finalContent = fileContent.replace(placeholder, content);
fs.writeFile(generatedFilename, finalContent, function (err) {
if (err) throw err;
});
});
}

This will simply write the output of the findfiles(), a concatenated string of content from all *.route files found.

Step 5: Clean up after yourself!

This step is unnecessary, but after I do the transformation and run the Webpack build process (which is a different monster I may explain in the future), I don’t really need a router.built.js file anymore. I’m cleaning up all generated files, and as they all contain “.built.” in their name, it’s easy to delete them.

helpers.deleteFiles('src/', '.built.');
console.time('* Deleted all .built. files from /src folder.');

The results of this little build process script

I’ve ended up with a router/index.built.js file which contains all routes configurations from chunked configs scattered across views folders. The script is very fast as it takes only a few milliseconds to complete and can run effortlessly before every build or watch if necessary. For convenience, I just have it in a preprocessor.js build script which fires before any build action on the application.

Example final router/index.js file

/*******
* THIS IS A TEMPLATE FILE FOR THE BUILD PROCESS, NOT THE FINAL FILE TO BE USED WITHE THE CODE!
* The below "marker" will be filled out every time build process runs
* It is like a pre-processor which will find all *.route files and create dynamic router configuration
*******/
import { createRouter, createWebHistory } from 'vue-router'const routes = [
{
path: '/about',
name: 'About',
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
},
{
path: '/SecondView',
name: 'SecondView',
component: () => import(/* webpackChunkName: "SecondView" */ '../views/SecondView/index.vue')
},
{
path: '/Thirdview',
name: 'Thirdview',
component: () => import(/* webpackChunkName: "Thirdview" */ '../views/Thirdview/index.vue')
},
{
path: '/Thirdview/something',
name: 'something',
component: () => import(/* webpackChunkName: "something" */ '../views/Thirdview/something/index.vue')
},
{
path: '/Thirdview/nothing',
name: 'Thirdview-nothing',
component: () => import(/* webpackChunkName: "hirdview-nothing" */ '../views/Thirdview/nothing/index.vue')
}
];const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
})
export default router

What we can do now, is in Webpack create an alias that will make sure whenever we call an import of the router configuration file, we can use router/index.js instead of router/index.built.js. Since there is only one place where we will call this config, and it will likely never change, you might want to skip it. I will :)

One thing to remember — do not be scared to build custom build scripts as there is so much to gain, and people rarely do that. It’s not hard, and everybody loves a person who made a whole’s team life easier ;)

— EDIT

Two days after writing this blog post, I realized that I can automate the whole process. I don’t need it at the moment and full automation would take away some flexibility, and as soon as I do that, I will realize I need it back.

The idea would — create a build script that goes through all /views/* folders and subfolders, searches for all index.vue files and use their paths to build out the final router path configuration. It would assume that every item in our views is publicly visible and accessible by a path, but it will forever bind URL paths with actual folder structure. There are good and bad sides to this approach, but it is worth considering it.

--

--

--

Software Engineering Changemaker. Driving digital transformation and sharing experiences and thoughts from my journey. 20 years and counting…

Love podcasts or audiobooks? Learn on the go with our new app.

Recommended from Medium

Speed up source-map generation with WebAssembly: Google Summer of Code 2018

Array陣列常用Method

The MERN Stack Explained

Setting up Server Side Auth with Supabase and NextJS

Top React Jobs — Week 47, 2020

React.js Jobs

vj for your home; a postmortem

DOM Manipulation — Node Styles and Text Nodes

Flask Bootstrap and Custom Bootstrap Theme

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Andrew Winnicki

Andrew Winnicki

Software Engineering Changemaker. Driving digital transformation and sharing experiences and thoughts from my journey. 20 years and counting…

More from Medium

When to use Multi-Page Apps?

Introducing the image resizer service

Image Resizing

Performance boost for Remirror positioners

Debugging Apollo Cache