Клиентский роутинг: как работает

Клиентский роутинг — механизм навигации в 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 — кнопки «Назад»/«Вперёд» не работают в самописном роутере.

Связанные темы

Ресурсы