I was building a web application for myself and as NPM packages and JS frameworks are getting bigger and more complicated, I decided not to install some JS framework, and build the app from scratch this time.
Creating a new web-app requires Router to handle the page changes, and this is my attempt on creating one.
So what does router really do for the web application.
The app should be able to read what URL is open and show the required content, so for example, I open a page www.mybook.com/user/1, the page should render the user 1, information.
The page should listen to URL changes, so when I click a button or an image, that redirects the user to www.mybook.com/post/my-latest-news the page will not refresh, but instead removes the old content, and renders the new required content. This way of rendering content is usually called single page application or SPA.
The page should have URL history memory, so when I press back or forward buttons in the browser, the application should know what pages to show.
I would like the router to have a possibility to define routes and fire some action, when the user lands on that route.
For example
router.on("/post/my-latest-news", (params) => {
// In here, I remove old content and render new one
})
- I would also like the router to accept parameters in the URL.
For example, "/post/:id"
would give me the id value as a parameter when deciding which post to show.
That's the basic of it, I think.
For listening to listening for route change, I will use the popstate listener API.
And for URL history, I am going to use browser History API
JavaScript Implementation
You can find the code for this router on Github
class Router {
constructor() {
this.routes = new Map();
this.current = [];
// Listen to the route changes, and fire routeUpdate when route change happens.
window.onpopstate = this.routeUpdate.bind(this);
}
// Returns the path in an array, for example URL "/blog/post/1" , will be returned as ["blog", "post", "1"]
get path() {
return window.location.pathname.split('/').filter((x) => x != '');
}
// Returns the pages query parameters as an object, for example "/post/?id=2" will return { id:2 }
get query() {
return Object.fromEntries(new URLSearchParams(window.location.search));
}
routeUpdate() {
// Get path as an array and query parameters as an object
const path = this.path;
const query = this.query;
// When URL has no path, fire the action under "/" listener and return
if (path.length == 0) {
this.routes.get('/')(path);
return;
}
// When same route is already active, don't render it again, may cause harmful loops.
if (this.current.join() === path.join()) return;
// Set active value of current page
this.current = path;
// Here I save the parameters of the URL, for example "/post/:page", will save value of page
let parameters = {};
// Loop though the saved route callbacks, and find the correct action for currect URL change
for (let [route, callback] of this.routes) {
// Split the route action name into array
const routes = route.split('/').filter((x) => x != '');
const matches = routes
.map((url, index) => {
// When the route accepts value as wildcard accept any value
if (url == '*') return true;
// Route has a parameter value, because it uses : lets get that value from the URL
if (url.includes(':')) {
parameters[url.split(':')[1]] = path[index];
return true;
}
// The new URL matches the saved route callback url, return true, meaning the action should be activated.
if (url == path[index]) return true;
return false;
})
.filter((x) => x);
// When the router has found that current URL, is matching the saved route name, fire the callback action with parameters included
if (matches.length == routes.length && routes.length > 0) {
callback({ path, parameters, query });
}
}
}
// Listen for route changes, required route name and the callback function, when route matches.
on(route, callback) {
this.routes.set(route, callback);
}
// Fire this function when you want to change page, for example router.change("/user/1")
// It will also save the route change to history api.
change(route) {
window.history.pushState({ action: 'changeRoute' }, null, route);
window.dispatchEvent(new Event('popstate'));
}
}
export default new Router();
Using the router
PS!
You should also, add
<base href="/">
in the header of your HTML file, so the front-end router, will always start the URL path from the start, and will not keep appending to the URL.
First, we import the Router
I am going to use ES6 native modules import, it's very easy and is supported by most browsers already.
import Router from '/libraries/router.js';
You can export router class from the file as new directly, or you could just do something like this
window.router = new Router()
PS!
My personal preference is to create the page as webcomponent or lit.js and then just swap the components when route is active.
Router.on('/home', (event) => {
// Replace and render page content here
});
Router.on('/post/:id', (event) => {
// Replace and render page content here
// You can get parameter with, event.parameters.id
});
Change routes
To change routes, you should use code below, because it will also store the URL change in browser history this way.
Router.change("/account")
Backend setup
When creating the SPA app on web, you should be aware of an error what might happen.
When trying to load the page for a URL, for example www.mybook.com/user/1, the backend usually sends 404
error, page not found.
That happens, because backend has not defined a route for /user/1
, the route finding for it, should happen on front-end side.
For fixing that, I redirect the 404 route on backend to index.html
file or whatever one you are using.
So instead of backend sending route not found, it will send the SPA app main file, and then the SPA app router will render the correct page, because it has the information about the routes.
Tools to use for back-end proxy
For debugging locally, I am using Node.js
and http-server
This console command, will run the http-server
on current folder and will redirect all failed requests to main index.html
and then JS router will take over.
http-server -p 8080 . --proxy http://localhost:8080?
For production, I am using Caddy as my backend proxy.
So here's a code-example how I send all 404 request to index.html
in Caddy.
The try_files
part, is where the failed routes are redirected.
https://www.mybook.com {
root * /srv/www/mybook
try_files {path} /index.html
encode zstd gzip
file_server
}
Top comments (0)