DEV Community

Cover image for Let's write a client-side hash router
Kacper Turowski
Kacper Turowski

Posted on

Let's write a client-side hash router

This will be a way more technical post than what I'm used to writing, so bear with me and feel free to point out all the dumb stuff I've managed to make an idiot out of myself with.

And since I'm the talkative type, there will be three parts to this post, so we can at least have this somewhat structured. Firstly, we'll have some short banter on the backstory of this concept. Secondly, bit longer blabber on the technical background of the issue. Finally, the juicy part - writing the code. Feel free to use the table of contents below to skip ahead.

  1. Banter on why did I need a custom router
  2. Boring technical blabber intro
  3. The juicy bits, let's write the code
  4. Wrap up

Why did I need a custom router?

So, I did a really fun project recently, one I already managed to brag about. Feel free to check it out here, although it's not that interesting, honestly. Proof of concept kind of thing, rather than a viable product. I made it to get some basic understanding of writing JavaScript code. But perhaps you'll want to have a look so you can have some context.

Anyway, please grab your beverage of choice and let's dissect the problem.

This project of mine, Litesite, is a purely client-side web application with custom URL routing. Well, almost purely client-side - it still needs to fetch some files from the webhost. But beyond that, all the logic of what to fetch and how to process it happens in the browser.

When I started toying with the idea of writing Litesite some good few months ago, I was endeared with GitHub Pages. They're free but they can only host static pages. While these days most hosting plans offer some kind of dynamicity (and I think that'll be PHP most of the time), the free or cheapest options might not offer this sort of functionality.

So you'd have to resort to writing a completely static website, file after file...

Or would you? (vsauce theme starts playing)

See, even if the webhost offers only static hosting, you still can embed JavaScript in your pages. And today, there's a, uh... 13th edition of ECMA Script out there? It offers functionalities that were thought impossible back when I used to write simple programs in Turbo Pascal for compsci lessons at secondary school.

So, I devised a plan to write something similar to Express.js that could run in the browser. It should offer dynamic experience with static pages. And to make things more complicated, it would be written in pure JavaScript. No external or downloaded scripts. Then I also decided to make it a CMS (or a blogging engine, more like) while retaining the ability to modify it as needed, to complicate things bit further.

(although it might've made things bit easier)

I realized quickly that I'd need some kind of custom URL routing. The alternative would be creating a separate file for each sub-page and that's... not really dynamic. And if it was supposed to be ablog, I'd have to write each page separatedly, putting same layout into every single file, duplicating just. So. Much. HTML.

The way around this? A single .html file that would somehow interpret what I want from it, then fetch relevant content and layouts from the webhost and render them on the client-side. If it sounds ominous, that's because it is don't worry - I'll try to cover more functionalities in next parts. But routing was the first thing I've created cause I knew that without it, there'd be no Litesite.

So...

Technical blabber intro

So, what exactly is a client-side hash router? Not a new concept, of course. It's been around for many long years now. But this doesn't really clear things up, does it.

It's a script that runs client-side (in the browser) and manages routing URLs (takes care of interpreting and handling requests to the server), and does it through use of hashes (fragment identifiers), since they don't cause page reloads.

If you're still in the dark (and I wouldn't blame you, as I re-read this while editing, hahah), let's dive in deeper, into the basics. If you have a clear understanding of this concept, skip ahead to the juicy bits.

Anyway.

URL Routing

(not to be confused with physical routing, which requires routers and cables and has something to do with networks and IPs and stuff)

In layman's terms, routing is a way in which web servers handle incoming requests. Let's assume you have a server or a VPS deployed somewhere on the Internet. You send a very basic request - say GET http://example.com/blog/my-first-post/index.html. That's the kind of stuff your browser does whenever you type in the address and hit Enter.

Your server's response will be "Okay, and..?"

Well, not exactly that. Most likely you'll get zero response actually, but the takeaway is that it won't know how to handle the request. Now, if you have basic web server software - Apache HTTP Server maybe - installed and running, your web server will intercept and handle that request.

By default, Apache uses file-based routing. That request above? Apache will look into the directory supposed to hold your website, try to find the index.html file inside blog/my-first-post/ directory and that's what you'll get back. And what your browser will try to render in its window.

This is fine and dandy, as long as you're fine with creating and managing a complex file structure. A single .html file for every sub-page. So much refactoring in case of changing the layout 😵‍💫

The way around this herculean task (or worse, sisyphean labor) is by using dynamic websites. Those are websites that serve you content that isn't static (duh... but what I mean is that the content doesn't live inside .html files but is prepared on the fly). The content and the looks depend on what address you request, what's put in the headers of the request, and the specifics of your browsing session. Even your physical location, IP address, timezone and current time, system language or browser window size could affect the end result.

Some years ago, query parameters were the industry standard for handling dynamic websites. You'd request http://example.com/blog/index.php?post=6453 and the PHP interpreter (also the industry standard back then and still insanely popular today) would slap the HTML layout together with the content from the a database before sending it back to you.

It's just that... the URL address could get ugly real fast. And it definitely ain't semantic.

Now, if you use custom URL routing, you can intercept the request yourself and make your app interpret it as you please. For example with http://example.com/blog/my-first-post, your router could extract some parameters from the route - blog would define which template should be used and my-first-post would denote the id of the post to be put into that template before being sent back.

Now this is a semantically clean and readable URL.

By the way, even default Apache routing config (the file-based one) does some custom routing. When you request http://example.com/blog/, the server will try to serve you index.php, index.html or index.htm from the blog/ directory. And there's a way to introduce customized routing through editing the HTACCESS file. But that's just a curio we won't really be talking about here.

Client-side routing

Routing or not, putting a new address in the browser makes it reload the page. And reloading means you request a new file from your static webhost. If you want these semantically clean URLs, you'll run into an issue - most likely there will be no blog/my-first-post/index.html file.

How do you avoid that? Well, there's some ways one could go about this. Firstly, the old way, through query parameters. Make your links look something like that: http://example.com/index.html?address=blog/my-first-post - and on the reload, your JavaScript code can take a look at the query parameters and interpret them accordingly.

But... that's a reload still. How can you implement a more streamlined experience, where you only change a part of the website, say content only?

Another way to handle this is through some pretty extensive scripting. Exchange your anchors (yes, anchors! that's what "a" in <a href=""> stands for and href means "Hypertext REFerence"! I didn't know this until now) for non-link <span onclick="navigate('blog/my-first-post')"> and then handle this in the JavaScript code. But... shucks pardner, what a chore and a non-natural way of writing websites. Additionally, you can't copy a link to a sub-page and send it to a friend. All content serving is based on scripts registering clicks.

So, is there a way around this?

Sure there is, otherwise I wouldn't make this huge buildup of tension leading to a hitchcockian plummet.

Through hash routing.

So what are hashes?

Ever notice a hashtag thingy in your browser's address bar? For example www.example.com/index.html#introduction or something like that? Those are called fragment identifiers (as they identify a specific fragment of the text - a heading, line, character, so on - but don't worry about that, we'll only focus on the basics).

By default, they link to objects with set id. How does it work? Well, let's look at an example below.

<h1>My cool website</h1>

<a href="#intro">Intro</a>
<a href="#about">About</a>
<a href="#summary">Summary</a>

<h2 id="intro">Introduction</h2></a>
<p>Intro text goes here...</p>

<h2 id="about">About the thing</h2></a>
<p>About text goes here...</p>

<h2 id="summary">Summary</h2></a>
<p>Summary text goes here...</p>
Enter fullscreen mode Exit fullscreen mode

Whenever you click an anchor that has href set to that hashtag thingy, the browser will scroll your window to the element with the same id as the hashtag (or: fragment identifier). Moreover, you can link another page and make it scroll the view on load to any named anchor in particular - simply by using <a href="www.example.com/product.html#about">. Neat, innit. Also, a reason why ids should be unique.

But how does that bring us closer to implementing routing?

See, the devil's in the details - whenever you try to follow a link to a fragment identifier that doesn't exist in the current page, nothing happens except for one thing. The address in the address bar of your browser changes. And that fires an event you can subscribe to.

Now, if you subscribe to that event, you'll be able to fetch the fragment identifier part of the URL and based on that, you can script the behavior of the website (loading new content and such).

Plenty websites use this kind of navigation (even in form of hash routing). Wikipedia lets you copy a link directly to a specific section of an article. Gmail links you to a specific folder in your email account. So on, so on.

To sum it up

Equipped with new knowledge, let's define client-side hash routing again.

It's a method of navigating the website's structure (different pages, different posts, etc) through custom URL addresses but without causing a website reload, since only fragment identifier anchors are used. Instead of reloading content, it relies on JavaScript to handle the process of fetching, processing and applying content to the existing view.

An obvious downside is that while you can fetch the content from the webhost with ease, you can't really interact with it otherwise. After all, you'd have to implement the entirety of CRUD on the front-end part, with passwords being in the plain sight.

So yeah, I am 100% positive you realize why this is a horrible, terrible idea.

Okay, let's write the code (finally)

This part should be relatively short, as we won't be implementing many functionalities into our basic router. That's to make sure I have something to keep writing about ensure that the code remains readable and easy to digest.

Boilerplate and events

Let's start with an empty project. We need a index.html boilerplate (or not even that, just the very basics) and a simple app.js script patched into it.

> index.html

<html>
    <head>
        <script src="app.js"></script>
        <title>My awesome hashrouter</title>
    </head>
    <body>
    </body>
</html>
Enter fullscreen mode Exit fullscreen mode

Yup, the body stays empty. That's it - the most basic HTML file I could think of. We'll do all our work in the app.js file, so I'm not gonna denote what code goes where from now on.

First, let's subscribe to the events that we need in order to make things work. There's actually two events we want to subscribe to. One is load, which fires whenever the page is loaded. We need this to load the content whenever the user loads or reloads the website, either with or without the hash route.

Second event is hashchange. This one fires every time the hash (fragment identifier) changes. We'll need this one because every non-external link on our website will point only to index.html, just with different fragment part.

window.addEventListener('load', onChangeRoute);
window.addEventListener('hashchange', onChangeRoute);
Enter fullscreen mode Exit fullscreen mode

Routing function scaffolding

Now, what should the onChangeRoute callback do? We'll write something really simple first, just a couple of calls we can fill out later on. We need a way to get the hash route from the address bar and we need to resolve it - get a callback function assigned to that route. And then we call that callback.

function onChangeRoute() {
    // first, we'll fetch the route from the address bar
    const path = getPathFromHashRoute();
    // then we'll resolve the route and get a callback function
    // and we need a way to register these routes too
    const route = resolveRouteFromPath(path);
    // and now we call the callback
    // this should do whatever is needed to redraw the content of the page
    route(); 
}
Enter fullscreen mode Exit fullscreen mode

Routes and registering them

Let's tackle some sample route callbacks first, so we can immediately try them out and then plug them into onChangeRoute logic when we eventually create it. These are probably the easiest to write too. We're just going to slap some basic text into our document view.

We'll also define a separate function to apply whatever HTML we generate onto the document. So code looks nicer and is easier to refactor.

function draw(html) {
    document.body.innerHTML = html;
}

function homeRouteHandler() {
    const content = '<h1>Home view</h1><p>This is my home view. Wanna see the <a href="/#about-me">about me</a> section?';
    draw(content);
}

function aboutmeRouteHandler() {
    const content = '<h1>This is about me</h1><p>I am J. Doe. I do websites for fun. This is my homepage. Go <a href="/">home</a>?';
    draw(content);
}
Enter fullscreen mode Exit fullscreen mode

Now that we have this, we can test it out. Open index.html in your browser and open developer tools. In the console, simply type homeRouteHandler() and smash [Enter]. You can do the same with aboutmeRouteHandler().

See? It just works!

Now, let's assign these two handler callbacks to actual routes. We can simply store references to the functions in an array, their keys being the routes themselves. We could even store anonymous functions that way but this simply won't look pretty.

const routes = [];

function registerRoute(path, callback) {
    routes[path] = callback;
}

registerRoute('/', homeRouteHandler);
registerRoute('/about-me', aboutmeRouteHandler);
Enter fullscreen mode Exit fullscreen mode

We can assign same handlers to several routes, just so you know. This could be useful later on, when we decide to add more functionalities to our router. Mainly, parsing route parameters. But that's not on today's menu!

Unfortunately, we can't test this out yet. We do have the routes registered, but we don't have a way to resolve them. As a sidenote, I'm using a naming system that's seen in Express.js and other apps that handle routes.

This isn't the simplest way to go about this, as the hash will be prefixed with # and not /. But I'm doing this to maintain consistent naming across products (like Express.js for example). You could name them however you wish and process those however you want, of course.

Resolving routes

Okay, we're back to our onChangeRoute function. Let's write the logic for realsies now. First, for getPathFromHashRoute, we need to extract the hash from the address bar. This is ridiculously simple, window object holds a beautiful property called location, which can tell you all about the contents of your address bar. We simply need the hash part, which luckily is available separatedly.

function getPathFromHashRoute() {
    let hash = window.location.hash;
    if(!hash) return '/';
    return '/' + hash.substring(1);
}
Enter fullscreen mode Exit fullscreen mode

It's that simple.

There's a safeguard in case the hash is not defined (say someone requests index.html). And like I said, we're prefixing the route with a forward slash to keep route naming convention consistent across products.

Resolving the hash route is just as simple to implement. We just fetch the callback from our array.

function resolveRouteFromPath(path) {
    const route = routes[path];
    if(!route) return routes['/'];
    return route;
}
Enter fullscreen mode Exit fullscreen mode

Again, we're safeguarding our router in case the requested route doesn't exist. In that case, we'll redirect the user to home view "/", which should be defined for every app anyway. Although you could definitely implement it differently, with a custom made 404 page or something else.

And that's it, in not even 50 lines of JavaScript code, we've managed to implement a client-side hash router. Reopen the index.html file and see the magic happen.

Wrap up

Here we are, at the end of our short journey through the basics of routing. Our beautiful router could use some improvements for sure, it's very lacking compared to what Express.js and others offer. But it works and today, that's all we've been aiming for. In some future part of these series, we'll talk about the more complex stuff. Like handling parameters.

And this... this wasn't a piece of cake to write. I'm still not sure how or why it works, but this is the reason why I'm writing these articles. Of course, I want to share this with you, dear reader. But writing is also a great way to cement your newfound knowledge.

So in case you've learned something really fun, consider writing an article yourself. And otherwise, until next time. Thanks for reading! 💖

Top comments (0)