Good day, friends!Simple, single-page apps based on React, Vue, or pure JavaScript surround us everywhere. A good "one-page" suggests an appropriate routing mechanism.Libraries such as "navigo" or "react-router" are very useful. But how do they work? Do we need to import the whole library? Or is it enough of some part, say, 10%? In fact, you can easily write a fast and useful router yourself, it will take a little time, and the program will consist of less than 100 lines of code.Requirements
Our router should be:- written in ES6 +
- compatible with history and hash
- reusable library
Typically, a web application uses one instance of a router, but in many cases we need several instances, so we won’t be able to use a singleton as a template. To work, our router requires the following properties:- routers: list of registered routers
- mode: hash or history
- root element: the root element of the application, if we are in history use mode
- constructor: the main function for creating a new router instance
class Router {
routes = []
mode = null
root = '/'
constructor(options) {
this.mode = window.history.pushState ? 'history' : 'hash'
if (options.mode) this.mode = options.mode
if (options.root) this.root = options.root
}
}
export default Router
Adding and Removing Routers
Adding and removing routers is done by adding and removing array elements:class Router {
routes = []
mode = null
root = '/'
constructor(options) {
this.mode = window.history.pushState ? 'history' : 'hash'
if (options.mode) this.mode = options.mode
if (options.root) this.root = options.root
}
add = (path, cb) => {
this.routes.push({
path,
cb
})
return this
}
remove = path => {
for (let i = 0; i < this.routes.length; i += 1) {
if (this.routes[i].path === path) {
this.routes.slice(i, 1)
return this
}
}
return this
}
flush = () => {
this.routes = []
return this
}
}
export default Router
Getting current path
We need to know where we are in the application at a certain point in time.For this, we need to process both modes (history and hash). In the first case, we need to remove the path to the root element from window.location, in the second - "#". We also need the (clearSlash) function to remove all routers (lines from start to finish):[...]
clearSlashes = path =>
path
.toString()
.replace(/\/$/, '')
.replace(/^\//, '')
getFragment = () => {
let fragment = ''
if (this.mode === 'history') {
fragment = this.clearSlashes(decodeURI(window.location.pathname + window.location.search))
fragment = fragment.replace(/\?(.*)$/, '')
fragment = this.root !== '/' ? fragment.replace(this.root, '') : fragment
} else {
const match = window.location.href.match(/#(.*)$/)
fragment = match ? match[1] : ''
}
return this.clearSlashes(fragment)
}
}
export default Router
Navigation
Ok, we have an API to add and remove URLs. We also have the opportunity to get the current address. The next step is navigating the router. We work with the “mode” property:[...]
getFragment = () => {
let fragment = ''
if (this.mode === 'history') {
fragment = this.clearSlashes(decodeURI(window.location.pathname + window.location.search))
fragment = fragment.replace(/\?(.*)$/, '')
fragment = this.root !== '/' ? fragment.replace(this.root, '') : fragment
} else {
const match = window.location.href.match(/#(.*)$/)
fragment = match ? match[1] : ''
}
return this.clearSlashes(fragment)
}
navigate = (path = '') => {
if (this.mode === 'history') {
window.history.pushState(null, null, this.root + this.clearSlashes(path))
} else {
window.location.href = `${window.location.href.replace(/#(.*)$/, '')}#${path}`
}
return this
}
}
export default Router
Watching the changes
Now we need logic to track address changes, either using a link or using the “navigate” method we created. We also need to ensure that the correct page is rendered on the first visit. We could use the state of the application to register changes, however, for the purpose of studying, we will do this with setInterval:class Router {
routes = [];
mode = null;
root = "/";
constructor(options) {
this.mode = window.history.pushState ? "history" : "hash";
if (options.mode) this.mode = options.mode;
if (options.root) this.root = options.root;
this.listen();
}
[...]
listen = () => {
clearInterval(this.interval)
this.interval = setInterval(this.interval, 50)
}
interval = () => {
if (this.current === this.getFragment()) return
this.current = this.getFragment()
this.routes.some(route => {
const match = this.current.match(route.path)
if (match) {
match.shift()
route.cb.apply({}, match)
return match
}
return false
})
}
}
export default Router
Conclusion
Our library is ready to use. It consists of only 84 lines of code!Code and usage example on Github .Thank you for attention.