Frontend routing

When building web applications, we will likely need to mutate the address the user sees within the browser to reflect the current resource being accessed. This is a core tenet of how browsers and HTTP work. An HTTP address should represent a resource. And so, when the user wishes to change the resource they are viewing, the address should correspondingly change. 

Historically, the only way of mutating the current URL within the browser would be for the user to navigate to a different page via an <a href> or similar. When SPAs started to become popular, however, JavaScript programmers needed to get creative. In the early days, a popular hack would be to mutate the hash component of a URL (example.org/path/#hash), which would give the user the experience of traversing a traditional website where each navigation or action would result in a new address and a new entry in the browser's history, hence enabling the use of the back-and-forward buttons in the browser.

The approach of mutating the #hash of a URL was famously used in Google's Gmail application when it launched in 2004 so that the address bar in the browser would accurately express what email or view you were currently looking at. Many other SPAs followed suit.

A few years later, thankfully, the History API found its way into browsers and is now the standard for mutating the address in response to navigations or actions within an SPA. Specifically, this API allows us to manipulate the browser session history by pushing new states or replacing current ones. For example, when a user expresses a wish to change to the About Us view within a fictional SPA, we can express this as a new state pushed to their history, like so:

window.history.pushState({ view: 'About' }, 'About Us', '/about');

This would immediately change the address in the browser to '/about'. Typically, the calling code would also instigate the rendering of the associated view. Routing is the name given to these combined processes of rendering the new DOM and mutating the browser's history. Specifically, a router takes responsibility for the following:

  • Rendering the view, component, or page that corresponds to the current address
  • Exposing an interface to other code so that navigation can be instigated
  • Listening for changes to the address made by the user (the popstate event)

To illustrate these responsibilities, we can create a simple router for an application that very simply displays Hello {color}! atop a background of that very color for any color represented in the path of the URL. Hence, /red will render a red background with the text, Hello red!. And /magenta will render a magenta background with the text, Hello magenta!:

And here is our implementation of colorRouter:

const colorRouter = new class {
constructor() {
this.bindToUserNavigation();

if (!window.history.state) {
const color = window.location.pathname.replace(/^//, '');
window.history.replaceState({ color }, color, '/' + color);
}

this.render(window.history.state.color);
}
bindToUserNavigation() {
window.addEventListener('popstate', event => {
this.render(event.state.color);
});
}
go(color) {
window.history.pushState({ color }, color, '/' + color);
this.render(color);
}
render(color) {
document.title = color + '!';
document.body.innerHTML = '';
document.body.appendChild(
document.createElement('h1')
).textContent = 'Hello ${color}!';
document.body.style.backgroundColor = color;
}
};
Notice how we're using the Class Singleton pattern here (as introduced in the last chapter). Our colorRouter abstraction is well-suited to this pattern as we need specific construction logic and we want to present a singular interface. We could have also used the Revealing Module pattern.

With this router, we can then call colorRouter.go() with our color and it'll change the address and be rendered as expected:

colorRouter.go('red');
// Navigates to `/red` and renders "Hello red!"

There is, even in this simple scenario, some complexity in our router. When the user originally lands on the page via conventional browsing, for example, perhaps by typing example.org/red into the address bar, the state of the history object will be empty, as we have not yet informed that browser session that /red is tied to the piece of state, { color: "red" }.

To populate this initial state, we need to grab the current location.pathname (/red) and then extract the color from it by removing the initial forward-slash. You can see this logic in the colorRouter constructor function:

if (!window.history.state) {
const color = window.location.pathname.replace(/^//, '');
window.history.replaceState({ color }, color, '/' + color);
}

For more complex paths, this logic can start to get quite complex. In a typical router, many different patterns of paths will need to be accommodated for. As such, usually, a URL parsing library will be used to properly extract each part of the URL and allow the router to route that address correctly.

It's important to use a properly constructed URL parsing library for use in production routers. Such libraries tend to accommodate all of the edge-cases implicit in URLs, and should ideally be compliant with the URI specification (RFC 3986). An example of this would be URI.js (available on npm as uri-js).

Various routing libraries and routing abstractions within larger frameworks have emerged over the years. They are all slightly different in the interface they present to the programmer. React Router, for example, allows you to declare your independent routes as a series of React components via JSX syntax:

function MyApplication() {
return (
<Router>
<Route exact path="/" component={Home} />
<Route path="/about/:employee" component={AboutEmployee} />
</Router>
);
}

Vue.js, a different framework, provides a unique routing abstraction of its own:

const router = new VueRouter({
routes: [
{ path: '/', component: Home }
{ path: '/about/:employee', component: AboutEmployee }
]
})

You may notice that, in both examples, there is a URL path specified as /about/:employee. The colon followed by a given token or word is a common way to designate that a specific portion of the path is dynamic. It's typical to need to dynamically respond to a URL that contains a piece of identifying information concerning a specific resource. It's reasonable that all of the following pages should produce different content:

  • /about/john
  • /about/mary
  • /about/nika

It would be incredibly burdensome to specify these all as individual routes (and near impossible with large datasets), so routers will always have some way of expressing these dynamic portions. The hierarchical nature of URLs is usually also mirrored in the declarative APIs provided by routers and will typically allow us to specify a hierarchy of components or views to render in response to such hierarchical URLs. Here's an example of a routes designation that could be passed to the Router service of Angular (another popular framework!):

const routes = [
{ path: "", redirectTo: "home", pathMatch: "full" },
{ path: "home", component: HomeComponent },
{
path: "about/:employee",
component: AboutEmployeeComponent,
children: [
{ path: "hobbies", component: EmployeeHobbyListComponent },
{ path: "hobbies/:hobby", component: EmployeeHobbyComponent }
]
}
];

Here, we can see that AboutEmployeeComponent is attached to the path of about/:employee and has sub-components that are each attached to the sub-paths of hobbies and hobbies/:hobby. An address such as /about/john/hobbies/kayaking would intuitively render AboutEmployeeComponent and within that would render EmployeeHobbyComponent.

You can probably observe here how intertwined a router is with rendering. It is indeed possible to have standalone routing libraries, but it's far more typical for frameworks to provide a routing abstraction themselves. This allows us to specify our routes alongside a view or component or widget, or whatever abstraction our framework provides for rendering things to the DOM. Fundamentally, although different on the surface, all of these frontend routing abstractions will achieve the same result. 

Another real-world challenge that many JavaScript programmers will expose themselves to, whether they're predominately working on the client side or server side, is that of dependency management. We'll begin exploring this next.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.135.247.181