Matheus Calegaro

(mais um) Blog sobre desenvolvimento e outras bobeiras.

Roteamento client-side no estilo SPA com JavaScript

Já ouviu falar em SPA? Não aquele que você vai para ficar (teoricamente) mais bonito(a), mas sim as Single Page Applications, que são aplicações web que lembram muito programas para desktop ou apps mobile nativos em questões de fluidez e dinamicidade de interações. Além disso, é notável a redução de recarregamentos de páginas quando utilizamos uma SPA devido ao fato de serem fortemente baseadas em requisições AJAX para carregar o conteúdo de cada “página”.

Mas se não recarregamos a página da forma natural, ou seja, pulando de uma URL para outra, como vamos gerenciar a transição de conteúdo na página e manter sua referência para o browser? A resposta é: implementando um roteador.

No mundo JavaScript temos muitas opções para essa solução (como já era de se esperar), mas vou focar em uma lib vanilla que não precisa de dependências para funcionar perfeitamente (até mesmo naquele monstro de sistema de 15 anos de idade que ninguém gosta de mexer), o Navigo. Ele tem uma API muito simples e painless que vai nos ajudar a configurar nossas rotas.

Estrutura de arquivos

Vamos seguir o padrão abaixo para organizar os arquivos e pastas do nosso projeto:

mathcale@tardis ~/Projetos/js-router $ tree .
.
├── assets
│   └── css
│       └── styles.css
├── index.html
└── src
    ├── main.js
    └── views
        ├── home.html
        └── sobre.html

4 directories, 5 files

Teremos um arquivo index.html na raiz do projeto que será o ponto de entrada da aplicação, onde todas as nossas views serão injetadas. Todos os JSs e outros componentes relacionados ao funcionamento principal da aplicação (core features) ficarão na pasta src. Os fru-frus e libs adicionais devem ficar na pasta assets, que no nosso exemplo só terá um styles.css para deixar o site um pouco mais agradável, sendo assim totalmente opcional.

Ponto de entrada - index.html

Em uma SPA toda a ação acontece a partir de um ponto de entrada, que nesse caso será o nosso arquivo index.html. O conteúdo de cada rota será injetado dentro de uma <div> a partir do JavaScript que escreveremos logo abaixo. A index.html deve ficar assim:

<!DOCTYPE html>

<html lang="pt-br">

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width,initial-scale=1" />

    <title>Roteador JS</title>

    <!-- Não obrigatório, só para deixar as coisas um pouquinho mais agradáveis -->
    <link rel="stylesheet" type="text/css" href="assets/css/styles.css" />

    <!-- Importando o Navigo a partir de uma CDN -->
    <script type="text/javascript" src="https://unpkg.com/navigo@6.0.2/lib/navigo.min.js"></script>
    <script type="text/javascript" src="src/main.js"></script>
</head>

<body>
    <nav>
        <div class="container">
            <a href="#!/home" class="brand">Seu Site</a>

            <ul>
                <li><a href="#!/home">Home</a></li>
                <li><a href="#!/sobre">Sobre</a></li>
            </ul>
        </div>
    </nav>

    <!-- Ponto de entrada da nossa app -->
    <div id="app" class="container"></div>
</body>

</html>

Cada um no seu quadrado - separação das views

Vamos modularizar as telas da nossa aplicação na pasta src/views. Nela teremos arquivos HTML simples com o conteúdo específico de cada página. Para este exemplo, eu criei duas views: a home.html e a sobre.html, e o conteúdo de cada uma é (respectivamente):

<h1>Homepage</h1>

<p>Esta é a homepage! Legal, né?</p>
<h1>Sobre</h1>

<p>Sobre o site...</p>

A aplicação em si - main.js

Disclaimer: estarei utilizando a sintaxe do ES6 que tenho certeza que tem suporte para os principais browsers do momento (Chrome, Firefox Quantum e Edge). Caso ainda não conheça as novidades do ES6, dê uma lida aqui e depois volte para este post. Lembrando que o nosso código vai ficar em src/main.js.

Disclaimer 2: poderíamos estar utilizando a jQuery para as manipulações do DOM “com mais agilidade”, mas meu ponto aqui é mostrar que você não precisa de jQuery para coisas tão triviais quanto inserir conteúdo em um elemento. Além disso, evitamos o carregamento de mais uma lib de muitos KBs onde iríamos utilizar nem 1% desses KBs carregados.

Antes de escrever o código, temos que pensar em como criar um pequeno ciclo de vida das nossas telas. Como nossa aplicação será de pequeno porte, a seguinte arquitetura será suficiente:

  1. Função para pegar o conteúdo de uma view e exibí-lo em uma <div> com um ID específico;
  2. Helper para retornar um elemento a partir de seu ID;
  3. Roteador faz suas ações com a ajuda das 2 funções acima;

Começando pelo helper, teremos a seguinte função que irá retornar um elemento que contém um ID específico (à lá jQuery só pra não perder o costume):

let $el = (elemId) => {
    return document.getElementById(elemId)
}

Agora vamos fazer a função que irá pegar o conteúdo de um template HTML (ou outro recurso de texto qualquer) e jogá-lo dentro de um elemento específico capturado pela nossa função $. Para isso, vamos utilizar o fetch, um substituto moderno do XMLHttpRequest baseado em Promises. Puro hype, né?

let getView = (url, elemId) => {
    fetch(url)
        .then(response => {
            if (response.ok) return response.text()
        })
        .then(viewContent => {
            // Injetamos a resposta resolvida pela Promise em um elemento
            $el(elemId).innerHTML = viewContent
        })
        .catch(exception => {
            // Promise rejeitada e irá lançar uma exceção
            $el(elemId).innerHTML = `Houve um erro ao carregar o conteúdo! :( Mais detalhes: ${exception}`
        })
}

Daora! agora vamos configurar o Navigo e as nossas rotas logo abaixo a função anterior:

/** 
 * Aqui passamos 3 parâmetros para a instância do Navigo:
 * 
 * 1. A URL principal da aplicação - no caso vamos deixar como null pois não temos a intenção de modificá-la
 * 2. Habilitamos o uso de hash na URL, um método antigo de roteamento e que serve de fallback para browsers que não possuem suporte à History API (pushState)
 * 3. O hash a ser passado nas URLs. Vamos usar '#!' pois é uma recomedação do Google para ter suas URLs reconhecidas pelos robôs da empresa. Mais info em https://developers.google.com/webmasters/ajax-crawling/docs/learn-more
 */
const router = new Navigo(null, true, '#!')

// Declaração das nossas rotas e suas respectivas views
router.on({
    'home': () => {
        getView('./src/views/home.html', 'app')
    },
    'sobre': () => {
        getView('./src/views/sobre.html', 'app')
    }
})

// Definimos a nossa rota principal da seguinte maneira:
router.on(() => {
    getView('./src/views/home.html', 'app')
})

router.notFound(query => {
    // Poderia ser um getView com o template do 404 também, mas estou com preguiça :p
    $el('view').innerHTML = 'Oops! A página que você solicitou não existe :('
})

// "Resolve" a rota
router.resolve()

Opcional - adicionando estilos

Isso aqui vai em assets/css/styles.css:

:root {
    --blue: #74b9ff;
    --white: #fff;
}

* {
    transition: all 300ms ease;
}

body {
    margin: 0;
    padding: 0;
    font-family: 'Roboto', Helvetica, sans-serif;
}

.container {
    padding: 0 60px;
}

nav {
    padding: 5px;
    background-color: var(--blue);
}

nav .brand,
nav ul,
nav ul li {
    display: inline-block;
    color: var(--white);
}

nav .brand {
    font-size: 21px;
    text-decoration: none;
}

nav .brand:hover {
    color: rgba(255, 255, 255, 0.8);
}

nav ul li a {
    margin-right: 20px;
    text-decoration: none;
    color: var(--white);
}

nav ul li a:hover {
    font-weight: bold;
}

Na minha máquina funciona…

É bom lembrar que precisamos de um servidor HTTP para podermos ver o resultado da nossa experiência. Se você usa GNU/Linux ou macOS, é possível utilizar o SimpleHTTPServer do Python na porta 8000. Se estiver no Windows, eu sinto muito você pode utilizar uma solução *AMP (WAMP, XAMPP) ou, se estiver no Windows 10, pode configurar o Apache nativamente no Windows Subsystem for Linux (WSL).

O comando para utilizar a solução Pythonesca é

cd /local/do/projeto
python -m SimpleHTTPServer 8000

E teremos nossa aplicação rodando em http://localhost:8000

Conclusão

É sempre bom conhecer as alternativas para as coisas que tomamos como verdade ou achamos ser o ideal para todas as situações. Vimos como é fácil criar uma aplicação no estilo das SPA, utilizamos novas tecnologias introduzidas no ES6 e vimos que dá para fazer requisições AJAX e manipulações no DOM com facilidade e sem a necessidade de usar a jQuery para isso.

Em projetos maiores, eu recomendo que você utilize uma solução mais robusta e com grande aceitação da comunidade como o Vue ou React para evitar dores de cabeça no futuro.

Referências:

Deep dive into client-side routing

Single-page Applications

krasimir/navigo: A simple vanilla JavaScript router with a fallback for older browsers

Can I Use… Support tables for HTML5, CSS3, etc

Comentários