Roteamento
O que é o Router?
Section titled “O que é o Router?”O Router do Slash é um sistema de roteamento para criar Single Page Applications (SPAs). Ele permite navegar entre diferentes “páginas” sem recarregar o browser, mantendo a URL sincronizada com o conteúdo exibido.
Características principais
Section titled “Características principais”- ✅ Modo History API ou Hash mode
- ✅ Parâmetros dinâmicos (
/users/:id) - ✅ Query strings (
?search=foo&page=2) - ✅ Navigation guards (proteção de rotas)
- ✅ Rotas aninhadas (nested routes)
- ✅ Metadados de rota (custom data)
- ✅ SSR-ready (Server-Side Rendering)
- ✅ Programmatic navigation (
push,replace,back,forward)
Criando um router
Section titled “Criando um router”Use createRouter() para criar uma instância do router:
import { createRouter } from '@_bashell/slash'import type { RouterConfig } from '@_bashell/slash'
const router = createRouter({ mode: 'history', // 'history' ou 'hash' routes: [ { path: '/', component: () => html`<h1>Home</h1>` }, { path: '/about', component: () => html`<h1>Sobre</h1>` } ]})Configuração do Router
Section titled “Configuração do Router”interface RouterConfig { /** Array de definições de rotas */ routes: RouteConfig[]
/** Modo do router (padrão: "history") */ mode?: 'history' | 'hash'
/** Path de fallback para 404 (padrão: null) */ fallback?: string
/** Guards globais de navegação */ guards?: NavigationGuard[]
/** Path inicial (para SSR) */ initialPath?: string}Definindo rotas
Section titled “Definindo rotas”Cada rota é definida por um objeto RouteConfig:
interface RouteConfig { /** Padrão do path (ex: "/users/:id") */ path: string
/** Componente a ser renderizado */ component: (state: RouterState) => Child
/** Nome da rota (opcional) */ name?: string
/** Metadados da rota */ meta?: RouteMeta
/** Guards de navegação específicos */ guards?: NavigationGuard[]
/** Rotas filhas/aninhadas */ children?: RouteConfig[]}Exemplo básico
Section titled “Exemplo básico”import { html, createRouter } from '@_bashell/slash'
const router = createRouter({ mode: 'history', routes: [ { path: '/', name: 'home', component: () => html` <div class="home"> <h1>Bem-vindo!</h1> <p>Esta é a página inicial</p> </div> ` }, { path: '/about', name: 'about', meta: { requiresAuth: false }, component: () => html` <div class="about"> <h1>Sobre nós</h1> <p>Conheça nossa história</p> </div> ` } ]})Componentes do Router
Section titled “Componentes do Router”<Router>
Section titled “<Router>”O componente Router renderiza o componente da rota atual e se atualiza a cada navegação. Use-o como componente, em qualquer posição do template:
import { html, render } from '@_bashell/slash'import { Router } from '@_bashell/slash/router'
const App = () => html` <div id="app"> <nav> <a href="/">Home</a> <a href="/about">About</a> </nav>
<main> <${Router} router=${router} /> </main> </div>`
render(html`<${App} />`, document.getElementById('root')!)A forma direta ${Router({ router })} também continua válida.
<Link>
Section titled “<Link>”O componente Link cria links de navegação que usam o router:
import { html } from '@_bashell/slash'import { Link } from '@_bashell/slash'
function Navigation({ router }) { return html` <nav> <${Link} to="/" router=${router}>Home</${Link}> <${Link} to="/about" router=${router}>About</${Link}> <${Link} to="/users/123" router=${router}>Perfil</${Link}> </nav> `}Link aceita somente caminhos do app: /x, ?q ou #h. Qualquer outra coisa (./x, ../x, about, //host, javascript:, https://...) nunca navega: o href vira about:blank#blocked e o dev mode avisa. Para um link externo de verdade, use a prop external:
html`<${Link} to="https://example.com/docs" external router=${router}>Docs<//>`// <a href="https://example.com/docs" rel="noopener noreferrer">Docs</a>external só libera http(s), mailto:, tel: e sms: e adiciona rel="noopener noreferrer". ?q e #h são relativos à página atual. O roteador só intercepta cliques em links http(s) da mesma origem (respeitando <base>); com ctrl, meta, shift ou alt, botão que não seja o esquerdo, target diferente de _self ou download, o navegador age normalmente. Barras invertidas em router.push() viram o caminho da mesma origem ('/\\evil' vai para /evil), e um push recusado pelo navegador rejeita com Navigation failed: ... sem dessincronizar o roteador. Veja Segurança.
Parâmetros dinâmicos
Section titled “Parâmetros dinâmicos”Rotas podem ter segmentos dinâmicos que extraem valores da URL:
const router = createRouter({ routes: [ { path: '/users/:id', component: (state) => { const userId = state.params.id return html` <div> <h1>Perfil do Usuário</h1> <p>ID: ${userId}</p> </div> ` } }, { path: '/posts/:postId/comments/:commentId', component: (state) => { const { postId, commentId } = state.params return html` <div> <h2>Post ${postId}</h2> <p>Comentário ${commentId}</p> </div> ` } } ]})Acessando parâmetros
Section titled “Acessando parâmetros”Os parâmetros estão disponíveis em state.params:
type RouteParams = Record<string, string>
// Exemplo: /users/123// state.params = { id: "123" }
// Exemplo: /posts/456/comments/789// state.params = { postId: "456", commentId: "789" }Query strings
Section titled “Query strings”Query strings são automaticamente parseadas e disponíveis em state.query:
const router = createRouter({ routes: [ { path: '/search', component: (state) => { const { q, page } = state.query // URL: /search?q=slash&page=2 // q = "slash", page = "2"
return html` <div> <h1>Busca: ${q || 'vazia'}</h1> <p>Página: ${page || '1'}</p> </div> ` } } ]})Navegação programática
Section titled “Navegação programática”O router expõe métodos para navegação programática:
// Navegar para uma nova rota (adiciona ao histórico)await router.push('/about')
// Substituir rota atual (não adiciona ao histórico)await router.replace('/login')
// Voltar na históriarouter.back()
// Avançar na históriarouter.forward()
// Ir para entrada específica na históriarouter.go(-2) // volta 2 páginasrouter.go(1) // avança 1 páginaExemplo prático
Section titled “Exemplo prático”import { html, createState } from '@_bashell/slash'
// O state fica fora do componente: se fosse criado dentro, seria recriado a cada renderconst credentials = createState({ email: '', password: '' })
function LoginForm({ router }) { const handleSubmit = async (e: Event) => { e.preventDefault()
const { email, password } = credentials.get()
try { const response = await login(email, password)
if (response.ok) { // Redirecionar para dashboard após login await router.push('/dashboard') } } catch (error) { console.error('Erro ao fazer login:', error) } }
return html` <form onSubmit=${handleSubmit}> <input type="email" placeholder="Email" onInput=${(e) => { const state = credentials.get() credentials.set({ ...state, email: (e.target as HTMLInputElement).value }) }} /> <input type="password" placeholder="Senha" onInput=${(e) => { const state = credentials.get() credentials.set({ ...state, password: (e.target as HTMLInputElement).value }) }} /> <button type="submit">Entrar</button> </form> `}Estado do Router
Section titled “Estado do Router”O router é um State<RouterState>, então você pode observar mudanças:
interface RouterState { /** Rota atual */ currentRoute: RouteMatch | null
/** Parâmetros da rota */ params: RouteParams
/** Query parameters */ query: RouteQuery
/** Metadados da rota */ meta: RouteMeta
/** Flag de navegação em progresso */ isNavigating: boolean}Reagindo a mudanças de rota
Section titled “Reagindo a mudanças de rota”import { createState } from '@_bashell/slash'
// Observar mudanças na rotarouter.watch((state) => { console.log('Navegou para:', state.currentRoute?.path) console.log('Params:', state.params) console.log('Query:', state.query)})
// Criar estado derivadoconst currentPath = createState('')router.watch((state) => { currentPath.set(state.currentRoute?.path || '/')})Exemplo completo: Blog SPA
Section titled “Exemplo completo: Blog SPA”import { html, render, createRouter } from '@_bashell/slash'import { Router, Link } from '@_bashell/slash'
// Componentes das páginasconst Home = () => html` <div> <h1>Blog Slash</h1> <p>Bem-vindo ao nosso blog!</p> </div>`
const PostList = () => { const posts = [ { id: 1, title: 'Introdução ao Slash' }, { id: 2, title: 'Roteamento Avançado' }, { id: 3, title: 'SSR com Slash' } ]
return html` <div> <h1>Posts</h1> <ul> ${posts.map(post => html` <li> <${Link} to=${`/posts/${post.id}`} router=${router}> ${post.title} </${Link}> </li> `)} </ul> </div> `}
const PostDetail = (state) => { const postId = state.params.id
return html` <div> <h1>Post #${postId}</h1> <p>Conteúdo do post...</p> <button onClick=${() => router.back()}> Voltar </button> </div> `}
// Criar routerconst router = createRouter({ mode: 'history', routes: [ { path: '/', component: Home }, { path: '/posts', component: PostList }, { path: '/posts/:id', component: PostDetail } ]})
// Renderizar appconst app = html` <div id="app"> <nav> <${Link} to="/" router=${router}>Home</${Link}> <${Link} to="/posts" router=${router}>Posts</${Link}> </nav>
<main> <${Router} router=${router} /> </main> </div>`
render(app, document.getElementById('root')!)Router no servidor (SSR)
Section titled “Router no servidor (SSR)”Passe initialPath para que o roteador resolva a rota de forma síncrona na criação (no servidor e na hidratação). No cliente, sem initialPath, ele usa window.location.
No servidor, Router funciona dentro de htmlString/renderToString: o HTML da rota é emitido de verdade, entre marcadores <!--reactive-start:id-->, e não é gravado no state serializado.
import { createRouter, Router } from '@_bashell/slash/router'import { htmlString, renderToString } from '@_bashell/slash/ssr'
const router = createRouter({ routes: [{ path: '/', component: () => htmlString`<h1>Home</h1>` }], initialPath: '/',})
const { html } = renderToString( () => htmlString`<main>${Router({ router })}</main>`,)// <main><!--reactive-start:s0--><h1>Home</h1><!--reactive-end:s0--></main>Veja SSR para o fluxo completo de renderização e hidratação.
Próximos passos
Section titled “Próximos passos”- Navigation Guards - Proteção e controle de navegação
- Rotas Aninhadas - Estrutura de rotas hierárquicas
- SSR com Router - Roteamento no servidor