Writing a Modern JavaScript Router

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.

All Articles