So many times, we might want to write our JavaScript code in a single file but we want the code to be executed only if a particular route is matched. You can achieve this with the help of a router by downloading a router library or writing the code yourself.
Today, i'll walk you through building a very basic router function with vanilla JavaScript. I'll be using some es6 features and javascript regular expressions for building this router, so you have to be familiar with them for better understanding.
The Concept
The good thing about programming is that you can solve a problem using any method or style you wish to but you have to avoid bad practices.
Here's the approach we'll take to building this router.
- Create a router class
- Create a method that stores the route logics and its corresponding callback function in an array.
- Create a method that processes these logics and return the corresponding callback function if the logic is true.
Here's a picture of what we want.
const router = new RouterClass();
// the get() method would store the '/' logic and callback in an array;
router.get('/', function(){
// code to be executed if '/' is matched
});
// here get() method would push '/another-page' and the callback to the existing array
router.get('/another-page', function(){
// code to be executed if '/another-page' is matched
);
router.init(); // this method will process the logics
Building our Router
Step 1 - create a router class
We'll create a class named Router that will be called with the new
keyword.
class Router {
}
Step 2 - add a constructor
The constructor is the method that is executed when our Router class is instantiated with the new keyword. In the constructor method, we'll create a property named routes
and assign an empty array to it.
The routes property will store all routes and their callback functions in an array.
class Router {
constructor(){
this.routes = [];
}
}
You can also pass an options
parameter to the constructor method and set some options for the router class but we'll skip that for the sake of simplicity.
Step 3 - Create a method for storing routes
We'll create a method named get()
for storing routes and it's callback. The get method should have two parameters: uri
and callback
class Router {
constructor(){
this.routes = [];
}
get(uri, callback){
}
}
I've named the method as get
for readability. Therefore, router.get(uri, callback);
should mean: get a particular uri and return a callback. You can name yours as you which. Maybe, router.if(uri, callback);
Step 4 - Validate parameters of the get method
In this method, we'll validate our parameters to make sure we don't mistakenly pass the wrong type of variables as parameters when using our router.
class Router {
constructor(){
this.routes = [];
}
get(uri, callback){
// ensure that the parameters are not empty
if(!uri || !callback) throw new Error('uri or callback must be given');
// ensure that the parameters have the correct types
if(typeof uri !== "string") throw new TypeError('typeof uri must be a string');
if(typeof callback !== "function") throw new TypeError('typeof callback must be a function');
// throw an error if the route uri already exists to avoid confilicting routes
this.routes.forEach(route=>{
if(route.uri === uri) throw new Error(`the uri ${route.uri} already exists`);
});
}
}
Step 5 - add route to the array of routes
After validating the parameter of the get()
method, we'll create an object named route
and push that object to our existing array of routes.
class Router {
constructor(){
this.routes = [];
}
get(uri, callback){
// ensure that the parameters are not empty
if(!uri || !callback) throw new Error('uri or callback must be given');
// ensure that the parameters have the correct types
if(typeof uri !== "string") throw new TypeError('typeof uri must be a string');
if(typeof callback !== "function") throw new TypeError('typeof callback must be a function');
// throw an error if the route uri already exists to avoid confilicting routes
this.routes.forEach(route=>{
if(route.uri === uri) throw new Error(`the uri ${route.uri} already exists`);
})
// Step 5 - add route to the array of routes
const route = {
uri, // in javascript, this is the same as uri: uri, callback: callback, avoids repition
callback
}
this.routes.push(route);
}
}
Step 6 - Process the routes with the init()
method
We are almost there! Let's process the routes using the init()
method. When this method is called, we'd want it to loop through our array of routes and match the route.uri
against the window.request.pathname
. If we find a match, we'd break out of the loop by returning the route.callback
function. To easily break out of the loop, we'll be using the Array.some()
method in place of Array.forEach()
because Array.some()
will end the loop when a truthy value is returned in the loop.
class Router {
constructor(){
this.routes = [];
}
get(uri, callback){
// ensure that the parameters are not empty
if(!uri || !callback) throw new Error('uri or callback must be given');
// ensure that the parameters have the correct types
if(typeof uri !== "string") throw new TypeError('typeof uri must be a string');
if(typeof callback !== "function") throw new TypeError('typeof callback must be a function');
// throw an error if the route uri already exists to avoid confilicting routes
this.routes.forEach(route=>{
if(route.uri === uri) throw new Error(`the uri ${route.uri} already exists`);
})
// Step 5 - add route to the array of routes
const route = {
uri, // in javascript, this is the same as uri: uri, callback: callback, avoids repition
callback
}
this.routes.push(route);
}
init(){
this.routes.some(route=>{
let regEx = new RegExp(`^${route.uri}$`); // i'll explain this conversion to regular expression below
let path = window.location.pathname;
if(path.match(regEx)){
// our route logic is true, return the corresponding callback
let req = { path } // i'll also explain this code below
return route.callback.call(this, req);
}
})
}
}
Very little code with some strange things going on there right? I'll start with the conversion to regular expression.
I converted our route.uri
to a regular expression because we'd want to match the exact value of the route.uri against the window.location.pathname
else router.get('/about', callback)
would match '/about-us', '/about-me', hence i introduced the regExp keywords ^
and $
.
You also noticed let req = { path }
which also means let req = { path: path }
. This is just to pass an object that can be accessible through our callback parameter. In practical, this means:
const router = new Router();
router.get('/about-me', function(req){
console.log(req.path); // outputs /about-me to the console
}
router.init();
Conclusion
These are the the steps you can reciprocate in building just a basic javascript router. For more advancement, you should target features like:
- having route parameters
- being able to assess query parameters
- having named routes
- grouping routes
If you don't know how to implement these, you can check out the source code of the router library i built, to see how i implemented some of these features. Better still, you can install the library via npm with npm i @kodnificent/sparouter
and use it in your script. Check out the install guide on npm.
Note
This is basically for frontend routing purposes. If you want to build a backend router, you can follow similar process but the process of getting the request uri would depend on the server.
This is my first post here on dev.to, so clicking the hearts will be very encouraging. Comments, contributions and criticism are highly welcomed. Check out my dev.to profile and give me a follow so we can dev together.
Nice :)
Maybe you can add a hash change event or something like that.
But I loved :)
Thanks.