Escrevendo um roteador JavaScript moderno

Bom dia amigos

Aplicativos simples de uma página baseados em React, Vue ou JavaScript puro nos cercam por toda parte. Uma boa "página única" pressupõe um mecanismo de roteamento apropriado.

Bibliotecas como "navigo" ou "react-router" são muito úteis. Mas como eles funcionam? Precisamos importar a biblioteca inteira? Ou é suficiente, em parte, por exemplo, 10%? De fato, você pode escrever facilmente um roteador rápido e útil, isso levará um tempo e o programa consistirá em menos de 100 linhas de código.

Exigências


Nosso roteador deve ser:

  • escrito em ES6 +
  • compatível com história e hash
  • biblioteca reutilizável

Normalmente, um aplicativo Web usa uma instância de um roteador, mas em muitos casos precisamos de várias instâncias, portanto não poderemos usar um singleton como modelo. Para funcionar, nosso roteador requer as seguintes propriedades:

  • roteadores: lista de roteadores registrados
  • mode: hash ou histórico
  • elemento raiz: o elemento raiz do aplicativo, se estiver no modo de uso histórico
  • constructor: a principal função para criar uma nova instância do roteador

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

Adicionando e removendo roteadores


A adição e remoção de roteadores é feita adicionando e removendo elementos da 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

Obtendo o caminho atual


Precisamos saber onde estamos no aplicativo em um determinado momento.

Para isso, precisamos processar os dois modos (histórico e hash). No primeiro caso, precisamos remover o caminho para o elemento raiz de window.location, no segundo - "#". Também precisamos da função (clearSlash) para remover todos os roteadores (linhas do início ao fim):

[...]

    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

Navegação


Ok, temos uma API para adicionar e remover URLs. Também temos a oportunidade de obter o endereço atual. O próximo passo é navegar pelo roteador. Trabalhamos com a propriedade "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

Observando as mudanças


Agora precisamos de lógica para rastrear as alterações de endereço, usando um link ou usando o método "navegar" que criamos. Também precisamos garantir que a página correta seja renderizada na primeira visita. Poderíamos usar o estado do aplicativo para registrar alterações, no entanto, para fins de estudo, faremos isso com 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

Conclusão


Nossa biblioteca está pronta para uso. Consiste em apenas 84 linhas de código!

Exemplo de código e uso no Github .

Obrigado pela atenção.

All Articles