Клиентский роутинг: как работает
Клиентский роутинг — механизм навигации в SPA, при котором браузер не делает запросы к серверу при переходах: JavaScript перехватывает клики, обновляет URL через History API и перерисовывает нужные компоненты.
Зачем нужно
Без клиентского роутинга SPA не может иметь несколько «страниц» с уникальными URL — это нарушает базовые ожидания пользователей (закладки, кнопка «Назад», шаринг ссылок). Понимание механизма помогает диагностировать проблемы: почему кнопка «Назад» не работает, почему при прямом заходе на /about возвращается 404, как реализовать анимации переходов.
Где используется
- react-router, vue-router, angular router — стандартные решения для фреймворков
- Самописные роутеры для микрофреймворков и vanilla JS
- Next.js App Router / Pages Router — файловый роутинг поверх клиентского
Как работает клиентский роутинг
1. Пользователь кликает <Link to="/about">О нас</Link>
2. React Router перехватывает клик, вызывает e.preventDefault()
3. history.pushState({}, '', '/about') — URL меняется без запроса к серверу
4. Router определяет какой компонент соответствует '/about'
5. React перерендеривает — страница обновляется
6. Пользователь нажимает "Назад" → popstate event
7. Router читает window.location.pathname = '/'
8. React перерендеривает предыдущую страницу
Реализация от нуля (без библиотек)
class Router {
constructor(routes) {
this.routes = routes; // { '/': HomeView, '/about': AboutView, ... }
// Перехват кликов по ссылкам с data-link
document.addEventListener('click', (e) => {
const link = e.target.closest('[data-link]');
if (link) {
e.preventDefault();
this.navigate(link.href);
}
});
// Обработка кнопок Назад/Вперёд
window.addEventListener('popstate', () => {
this.render(window.location.pathname);
});
}
navigate(url) {
history.pushState(null, '', url);
this.render(window.location.pathname);
}
render(path) {
// Находим совпадение с учётом параметров
const route = this.matchRoute(path);
const app = document.getElementById('app');
if (route) {
const { component, params } = route;
app.innerHTML = component(params);
} else {
app.innerHTML = '<h1>404 — Не найдено</h1>';
}
}
matchRoute(path) {
for (const [pattern, component] of Object.entries(this.routes)) {
// Простое сопоставление: /users/:id → /users/42
const paramNames = ;
const regexStr = pattern.replace(/:([^/]+)/g, (_, name) => {
paramNames.push(name);
return '([^/]+)';
});
const match = path.match(new RegExp(`^${regexStr}$`));
if (match) {
const params = Object.fromEntries(
paramNames.map((name, i) => [name, match[i + 1]])
);
return { component, params };
}
}
return null;
}
start {
this.render(window.location.pathname);
}
}
// Использование
const router = new Router({
'/': => '<h1>Главная</h1><a href="/about" data-link>О нас</a>',
'/about': => '<h1>О нас</h1><a href="/" data-link>Назад</a>',
'/users/:id': ({ id }) => `<h1>Профиль #${id}</h1>`,
});
router.start();
React Router: под капотом
// React Router использует History API аналогично
// BrowserRouter слушает popstate и обновляет контекст
// Link вызывает history.push вместо перехода браузера
// Routes + Route находят совпадение и рендерят компонент
import { BrowserRouter, Routes, Route, Link, useNavigate } from 'react-router-dom';
function App() {
return (
<BrowserRouter>
<nav>
<Link to="/">Главная</Link> {/* preventDefault + pushState */}
<Link to="/about">О нас</Link>
</nav>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</BrowserRouter>
);
}
// Программная навигация
function LoginButton() {
const navigate = useNavigate;
const handleLogin = async () => {
await login;
navigate('/dashboard', { replace: true }); // replaceState
};
return <button onClick={handleLogin}>Войти</button>;
}
Частые ошибки
- Не настроен server fallback — при прямом заходе на
/aboutсервер ищет файлabout/index.html, не находит → 404; настройтеtry_filesв nginx. - Использование обычного
<a href>вместо<Link>— браузер делает полный переход к серверу. - Нет обработки
popstate— кнопки «Назад»/«Вперёд» не работают в самописном роутере.