Escribir un enrutador JavaScript moderno

¡Buen dia amigos!

Las aplicaciones simples de una sola página basadas en React, Vue o JavaScript puro nos rodean en todas partes. Una buena "una página" supone un mecanismo de enrutamiento apropiado.

Las bibliotecas como "navigo" o "react-router" son muy útiles. Pero, como trabajan? ¿Necesitamos importar toda la biblioteca? ¿O es suficiente de alguna parte, digamos, 10%? De hecho, puede escribir fácilmente un enrutador rápido y útil, tomará un poco de tiempo y el programa constará de menos de 100 líneas de código.

Requisitos


Nuestro enrutador debe ser:

  • escrito en ES6 +
  • compatible con historia y hash
  • biblioteca reutilizable

Normalmente, una aplicación web usa una instancia de un enrutador, pero en muchos casos necesitamos varias instancias, por lo que no podremos usar un singleton como plantilla. Para funcionar, nuestro enrutador requiere las siguientes propiedades:

  • enrutadores: lista de enrutadores registrados
  • modo: hash o historial
  • elemento raíz: el elemento raíz de la aplicación, si estamos en modo de uso histórico
  • constructor: la función principal para crear una nueva instancia de enrutador

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

Agregar y quitar enrutadores


La adición y eliminación de enrutadores se realiza agregando y eliminando elementos de matriz:

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

Obteniendo la ruta actual


Necesitamos saber en qué punto de la aplicación estamos en un momento determinado.

Para esto, necesitamos procesar ambos modos (historial y hash). En el primer caso, necesitamos eliminar la ruta al elemento raíz de window.location, en el segundo - "#". También necesitamos la función (clearSlash) para eliminar todos los enrutadores (líneas de principio a fin):

[...]

    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

Navegación


Ok, tenemos una API para agregar y eliminar URL. También tenemos la oportunidad de obtener la dirección actual. El siguiente paso es navegar por el enrutador. Trabajamos con la propiedad "mode":

[...]

    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

Viendo los cambios


Ahora necesitamos lógica para rastrear los cambios de dirección, ya sea usando un enlace o usando el método de "navegación" que creamos. También debemos asegurarnos de que se muestre la página correcta en la primera visita. Podríamos usar el estado de la aplicación para registrar cambios, sin embargo, con el fin de estudiar, haremos esto con 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

Conclusión


Nuestra biblioteca está lista para usar. ¡Consta de solo 84 líneas de código!

Código y ejemplo de uso en Github .

Gracias por su atención.

All Articles