Skip to main content

Write your own router


In my formative years as a coder and architect I was mentored by several very talented engineers. One of them helped me understand a general goal of all engineering: simplicity. The best engineering is elegant in it's simplicity. Vehicles are my favorite example. The wheel itself? Incredibly simple, cheap and by far the most important component of a rolling vehicle. The internal combustion engine? A massively complex, polluting machine which we still struggle with more than 100 years after it's invention. I am so excited to see the electric motor become the heart of the automobile!

In software engineering our goal is to create highly functional systems without creating great complexity. This is hard enough in itself but sometimes we needlessly create complexity by misuse (or lack of use) of borrowed code (e.g., libraries).  Good engineers are always on the lookout for borrowed code which solves a problem. The boon in open source in the last couple decades has made this a routine exercise for most good engineering teams. There are, however, times when we jump too quickly to a library instead of writing it ourselves. Using borrowed code always come with some minimal overhead: dependency on a third party, debugging difficulty if it fails, red-herring analysis if not all paths are well tested, etc. Borrowed code can also require learning the vernacular of that code. Learning the lingo of the library is often worth the effort: once we understand how to interact with the library or framework we are, assumably, more efficient in the remaining work.

Sometimes, however, the use of borrowed code causes us to learn the library's mechanism instead of simply writing the same code ourselves. This is unfortunate because, in doing so, we making it more difficult to customize the behavior to our own purposes and often understand less about what we really should have understood in the first place.

A good example of this is javascript SPA (single page app) routers. Do a google search for routers and you will find a plethora of open source routers. The problem with these routers is that their configurations generally take as much code as writing your own, they are sometimes awkward to customize and they attempt to help you with problems you don't have. Why are you learning to use somebody else's configuration when you can just write your own fully customizable (because it is YOUR code) router in the same amount of time? Worse, some routers make it difficult to introduce conditional logic, use query string values, multi-purpose the URL and other woes. This YOUR app. You should be in control.

I've included some code in this blog for a very simple querystring pushstate router to demonstrate how easy this is. Before describing the code it is worth discussing the rationale for querystring pushstate.  If you've spent some time doing routing in javascript you probably understand there are 2 basic strategies: hashtags and pushstate.  Hashtags are old school now.  Hashtag routing is the re-purposing of a very old mechanism in HTML which allows for intra-page navigation. I won't bore you with the history lesson but suffice it to say they weren't really designed for what we are now using them for. However, because we didn't have an alternative most of our SPAs have been built with this routing approach. One of the problems with hashtags (officially called "fragment identifiers" BTW) is that they are not part of the URL. When you go to http://animals.com/index.html#cats the #cats is NOT sent to the server in the HTTP request. Therefore, if the server does any redirection (e.g., for a single sign on solution) the hashtag is lost (and thus the app will default to it's default route).

Pushstate is an approach introduced in HTML5 which was designed for SPA routing. Pushstate allows your app to "push" URLs on the history stack without causing the page to reload. Likewise when the back button is clicked these "pushed" URLs are evented to the app instead of causing a page load. Pushstate took awhile to get adopted because of browser compatibility but it is now very well supported.  This blog post is not a tutorial on pushstate so I won't spend time on details but the implementation is very straight forward. I encourage your to read a quick tutorial or read the spec.  It is easy stuff.  The problem with pushstate is that teams attempt to use it by altering the URL path as a mechanism to communicating the route. Example: http://animals.com/cats.  This works great until the user clicks the browser refresh button. Now the browser sends this URL to the web server (e.g, apache) and gets a 404 because the server doesn't have a /cats path. There are ways to configure the web server to solve this problem but the question is why? There is a better approach.

IMO the best routing approach is querystring pagestate routing. In this approach querystring parameters are used to communicate route. Example: http://animals.com/index.html?route=cats. Query string parameters are sent to the host so redirections work.  The page itself does not change so apache knows how to serve it. Best of all it is ALREADY a parameter model.  I can implement as many parameters as I like for handling any special conditions. There is no need to do any special parsing.  You can build as simple or complex model as you like. You can also multi-purpose it with other state-oriented parameters. YOU are in control.

Example query-string pagestate router:
 

class Router {

   // PUBLIC

   start() {
      window.addEventListener('popstate', () => handleRouteToUrl(location.href));
      handleRouteToUrl(location.href);
   }

   routeTo(routeName, replace=false) {
      let url = location.href.split('?')[0] + `?route=${routeName}`;
      replace ? history.replaceState({}, null, url) : history.pushState({}, null, url);
      handleRouteToUrl(url);
   }

   // PRIVATE
   
   handleRouteToUrl(url) {
      const routeName = getQueryParam(url, 'route') || '';
      switch (routeName) {
         case "cats":
            handleCats();
            break;
         case "dogs":
            handleDogs();
            break;
         case "in-laws":
            handleInLaws();
            break;
         default:
            handleCats();
      }
   }

   getQueryParam(url, name) {
      const matches = url.match(new RegExp('[?&]' + name + '=([^&#]*)'));
      return matches && matches[1];
   }

   handleCats() {
      // ... render cats
   }
}


To use this router simply call the start() when the app loads. It will route based on the URL when the page loads...e.g. http://cute-animals/index.html?route=cats would route to cats.

When you want to navigate in your app just call the router's routeTo function.

router.routeTo('cats')

Calling the routeTo in this fashion will add the route to the browser history as is normally desired.  If you want to "replace" the current URL (substitute the new URL in the history) then indicate so:

router.routeTo('cats', true)

That is all it takes to write your own router.  You can add any additional handling you need.  For instance, perhaps you want other context on the URL (e.g., http://cute-animals/index.html?route=cats&age=kitty).  It is your script.  You can do anything you want instead of figuring out how to make it work in the borrowed code.

Comments

Popular posts from this blog

Managing Risk and the Shadow Backlog

As an approach to developing software agile has had a profound impact.  There are many great architectures, languages, libraries, frameworks, etc. that have helped software engineering get better. But in my experience almost none of these has been as important as the process of agile. There are many advantages to agile but one of the most important but least articulated (especially to executive leadership...who should care the most) is risk management.  Unfortunately, this aspect of agile can also be misunderstood by the practitioners: the development and test team. So why does agile reduce risk?  Because the agile process creates transparency.  Nothing creates more risk than hiding information.  Nobody can make a decision to reduce/avoid risk if they are not operating with good information.  This is not to suggest that teams who are developing using waterfall approaches are intentionally hiding the truth of their project's progress.  Rather, the p...

Why is Performance Important?

I often lament the transition from desktop to web application development. The web development ecosystem was so inferior to the desktop that we are still trying to catch up with the state of the art 20 years ago. One of the earliest victims of the move to web was performance. In desktop apps the expectation was immediate (maximum of a hundred milliseconds) response times. When the world wide web arrived we were suddenly facing 10-20 second response times. Dark days for user experience. Since the late 90s the web has slowly improved. We are now able to experience near-desktop performance with our web apps. Most of this is due to dramatically improved bandwidth. Another contribution was the move to ajax, web services and single page apps. Rather than making page requests for each transition we are now making very small, efficient data requests.  In spite of these improvements I still find teams setting very low expectations for performance (example: "3 second" service ...

We need a new front-end language

I like to preach to software engineers that       the essence of software engineering is    designing for complexity . I also preach that       coding = design . So transitively speaking       coding = designing for complexity . I deeply believe this (and you are going to hear it often in this blog).  While there are many important skills a coder brings to the job designing for complexity (DC henceforth) is our bread and butter. Yes, there are other important talents we bring to coding... Performant algorithms, Cool animations Understanding new and adjacent new technologies etc. These are important and often are the most fun part of the programming job but we spend most of our time (80%+) on DC.  A good programmer is thinking about design when writing every line of code.  "How will it relate to the other code in this app?", "how will it stand-up to change?", "will the next developer (or even me 6 month...