Если вы когда-нибудь замечали, что сайт работает как нативное приложение — страницы переключаются без перезагрузки, интерфейс реагирует мгновенно, — скорее всего, вы имеете дело с одностраничным приложением (SPA). В этой статье я расскажу, как такие штуки устроены изнутри, и покажу на примерах, как собрать своё первое SPA на чистом JavaScript. Никаких сложных фреймворков, только суть.
Что такое SPA и почему это удобно
Представьте, что вы приходите в кафе. В обычном сайте — вы каждый раз выходите на улицу, чтобы зайти в другую дверь. В SPA — вы сидите за столиком, а официант приносит вам новые блюда. Вот эта «смена блюд без выхода из-за стола» и есть суть одностраничного приложения.
Технически SPA — это веб-страница, которая после первой загрузки не перезагружается полностью. Вместо этого JavaScript подгружает новые данные и обновляет только нужные части интерфейса. Благодаря этому всё работает быстро и плавно, как в десктопной программе.
Главные плюсы SPA:
- скорость — после первой загрузки ресурсы кэшируются, и переходы происходят мгновенно;
- отзывчивость — интерфейс реагирует на действия пользователя без задержек на перезагрузку;
- удобство — можно делать сложные интерактивные интерфейсы, как в Google Maps или Trello.
Но есть и минусы: первая загрузка может быть тяжёлой, а для поисковиков SPA — тёмный лес, если не настроить серверный рендеринг. Но об этом позже.
Как устроено SPA изнутри
В основе любого SPA лежат три кита: HTML, CSS и JavaScript. Но есть ещё одна важная деталь — маршрутизация. В обычном сайте адрес меняется, когда вы переходите на другую страницу. В SPA адрес тоже меняется, но страница не перезагружается. Как так?
Секрет в History API. Это специальный механизм браузера, который позволяет менять URL без перезагрузки. Вызываешь метод pushState — и адрес в строке браузера меняется, а страница остаётся как есть. Затем JavaScript смотрит на новый адрес и решает, что показать.
Представьте, что вы переключаете каналы на телевизоре пультом. Канал меняется, но сам телевизор не выключается. Вот и здесь — адрес меняется, но приложение продолжает работать в том же окне.
Кроме маршрутизации, в SPA важно управлять состоянием. Состояние — это все данные, которые приложение хранит в памяти: что пользователь ввёл в форму, какие элементы открыты, какие данные загружены с сервера. Когда состояние меняется, интерфейс должен обновиться. Это называется реактивностью.
В чистом JavaScript реактивность делается вручную: вы вешаете обработчики событий, меняете DOM, следите за тем, чтобы всё было синхронно. В фреймворках типа React или Vue это автоматизировано, но для понимания сути полезно попробовать руками.
Пишем простое SPA на чистом JavaScript
Хватит теории, давайте кодить. Создадим мини-приложение с двумя «страницами»: главная и о нас. Без сервера, просто чтобы понять принцип.
Сначала разметка. У нас будет контейнер, куда мы будем вставлять содержимое страниц, и две ссылки в шапке.
<!-- index.html -->
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Моё первое SPA</title>
</head>
<body>
<header>
<nav>
<a href="/" data-link>Главная</a>
<a href="/about" data-link>О нас</a>
</nav>
</header>
<main id="app"></main>
<script src="app.js"></script>
</body>
</html>
Обратите внимание на атрибут data-link. Мы будем использовать его, чтобы перехватывать клики по ссылкам и не давать браузеру перезагружать страницу.
Теперь JavaScript. Создадим простую маршрутизацию на основе hash — это когда адрес выглядит как index.html#/about. Такой подход проще для начала, потому что не требует настройки сервера.
// app.js
const app = document.getElementById('app');
// Функция, которая возвращает HTML для каждой страницы
function renderPage(route) {
if (route === '/about') {
return 'О нас
Мы маленькая команда, которая любит JavaScript.
';
}
return 'Главная
Добро пожаловать! Это простое SPA на чистом JS.
';
}
// Обработчик изменения hash
function handleRouteChange() {
const hash = window.location.hash || '#/';
const path = hash.replace('#', '');
app.innerHTML = renderPage(path);
}
// Слушаем событие изменения hash
window.addEventListener('hashchange', handleRouteChange);
// Перехватываем клики по ссылкам с data-link
document.addEventListener('click', (e) => {
if (e.target.matches('[data-link]')) {
e.preventDefault();
window.location.hash = e.target.getAttribute('href');
}
});
// Первоначальная загрузка
handleRouteChange();
Вот и всё. Теперь при клике на ссылку меняется hash, срабатывает событие hashchange, и мы обновляем содержимое контейнера. Никакой перезагрузки страницы.
Этот пример очень простой, но он показывает главную идею SPA: мы сами решаем, что показывать, и обновляем только нужную часть страницы.
Как работать с данными и состоянием
В реальном приложении нам нужно не только показывать статичные страницы, но и работать с данными. Допустим, у нас есть список задач. Мы хотим отображать его и позволять пользователю добавлять новые.
Состояние — это просто объект или массив, который хранит данные. При изменении состояния мы должны обновить интерфейс. В чистом JS это делается вручную.
Вот пример: у нас есть массив задач, и мы выводим его на страницу. Когда пользователь добавляет задачу, мы обновляем массив и вызываем функцию рендера.
// состояние
let todos = ['Изучить JavaScript', 'Написать статью'];
function render() {
const list = document.getElementById('todo-list');
list.innerHTML = todos.map(todo => `${todo} `).join('');
}
// добавление новой задачи
function addTodo() {
const input = document.getElementById('todo-input');
const text = input.value.trim();
if (text) {
todos.push(text);
input.value = '';
render();
}
}
Здесь мы вручную следим за тем, чтобы после каждого изменения массива вызывался render(). В больших приложениях такая ручная синхронизация становится ошибкоопасной. Поэтому и придумали фреймворки, которые автоматически обновляют интерфейс при изменении состояния.
Но даже на чистом JS можно организовать состояние более аккуратно. Например, вынести его в отдельный объект с методами, которые уведомляют подписчиков об изменениях. Это называется паттерн Observer.
Проще говоря, вы создаёте хранилище, которое умеет оповещать компоненты, когда данные изменились. Такой подход лежит в основе Redux и Vuex, только там всё сделано за вас.
Сравнение подходов: чистый JS, React, Vue
Когда вы решите перейти на фреймворки, у вас будет выбор. Давайте сравним основные варианты, чтобы вы понимали, что к чему.
| Подход | Плюсы | Минусы |
|---|---|---|
| Чистый JS | Нет зависимостей, полный контроль, легче понять основы | Много ручного кода, легко запутаться в больших проектах |
| React | Компонентный подход, огромное сообщество, виртуальный DOM | Кривая обучения, нужно изучать JSX и экосистему |
| Vue | Проще для новичков, шаблоны похожи на HTML, хорошая документация | Меньше вакансий, чем React, но всё равно востребован |
Если вы новичок, я советую начать с чистого JS. Он даст вам фундамент, на котором потом легко освоить любой фреймворк. А если хочется сразу делать что-то серьёзное, но с меньшим порогом входа — присмотритесь к Vue.
React — это индустриальный стандарт, его учат на курсах, о нём пишут тонны статей. Но не забывайте, что это просто инструмент. Главное — понимать принципы, а они одинаковы везде.
Куда двигаться дальше
Теперь, когда вы знаете основы SPA, можно двигаться дальше. Вот несколько направлений, которые стоит изучить:
- Виртуальный DOM — как React и Vue ускоряют обновление интерфейса;
- Компонентный подход — как разбивать интерфейс на переиспользуемые куски;
- Роутинг на сервере — как настроить сервер, чтобы SPA работало с реальными URL, а не с hash;
- SSR (серверный рендеринг) — как сделать SPA дружелюбным для поисковиков.
Не пытайтесь объять необъятное. Идите по шагам: сначала поймите, как работает маршрутизация, потом состояние, потом всё остальное.
Итог
Одностраничные приложения — это не магия, а просто набор техник, которые позволяют сделать веб-интерфейс быстрым и отзывчивым. Главное — понять принципы: маршрутизация без перезагрузки, управление состоянием и обновление DOM.
Начните с малого — напишите простое SPA на чистом JavaScript. Поэкспериментируйте с hash-роутингом, добавьте работу с данными. Когда почувствуете уверенность, переходите к фреймворкам. И помните: никакой фреймворк не спасёт, если вы не понимаете, что происходит под капотом.
Удачи в кодинге!



