В традиционных сайтах каждая страница — отдельный HTML-документ. В SPA (Single Page Application) страница одна, а маршрутизация симулируется через JavaScript. History API — это механизм браузера, который позволяет менять URL без перезагрузки страницы.
Два ключевых метода:
// pushState — добавляет новую запись в историю
history.pushState(state, title, url)
// state — объект состояния (до 640 КБ)
// title — устарел, браузеры игнорируют
// url — новый URL (должен быть того же домена)
history.pushState({ page: 'about' }, '', '/about')
// URL изменился на /about, страница НЕ перезагрузилась
// replaceState — заменяет текущую запись (не добавляет в историю)
history.replaceState({ page: 'home' }, '', '/')history.back() // кнопка Назад
history.forward() // кнопка Вперёд
history.go(-2) // на 2 шага назад
history.go(1) // на 1 шаг вперёд
history.go(0) // перезагрузить страницуСрабатывает когда пользователь нажимает Назад/Вперёд или вызывается history.go(). НЕ срабатывает на pushState/replaceState:
window.addEventListener('popstate', (event) => {
console.log('Переход:', location.pathname)
console.log('Состояние:', event.state)
renderPage(location.pathname)
})Hash-роутинг: example.com/#/about
# не отправляется на серверhashchangeHistory-роутинг: example.com/about
#popstateconst routes = {
'/': () => '<h1>Главная</h1>',
'/about': () => '<h1>О нас</h1>',
'/users/:id': ({ id }) => `<h1>Пользователь ${id}</h1>`,
}
function navigate(path) {
history.pushState({}, '', path)
render(path)
}
function render(path) {
const handler = matchRoute(path, routes)
document.getElementById('app').innerHTML = handler ? handler() : '404'
}
window.addEventListener('popstate', () => render(location.pathname))pushState может сохранять объект состояния (до 640 КБ), который возвращается в event.state при popstate:
history.pushState({ scrollY: 450, filters: ['js', 'python'] }, '', '/articles')
window.addEventListener('popstate', (e) => {
if (e.state?.scrollY) window.scrollTo(0, e.state.scrollY)
})Перед переходом можно проверить условия (например, несохранённые изменения):
function guardedNavigate(path) {
if (hasUnsavedChanges() && !confirm('Есть несохранённые изменения. Уйти?')) {
return false
}
navigate(path)
return true
}React Router и Vue Router используют History API под капотом. useNavigate() в React Router вызывает history.pushState. <router-link> в Vue Router делает то же самое.
Мини-роутер с матчингом маршрутов, гвардами навигации и симуляцией истории браузера
// Мини-роутер с поддержкой параметров и гвардов
class MiniRouter {
constructor() {
this._routes = new Map()
this._history = ['/']
this._historyIndex = 0
this._guards = []
this._listeners = []
}
// Регистрация маршрута (поддержка параметров :param)
route(pattern, handler) {
this._routes.set(pattern, handler)
return this
}
// Гвард навигации — может отменить переход
addGuard(guardFn) {
this._guards.push(guardFn)
}
// Сопоставление URL с паттерном, извлечение параметров
_matchRoute(path) {
for (const [pattern, handler] of this._routes) {
const paramNames = []
const regexStr = pattern.replace(/:([^/]+)/g, (_, name) => {
paramNames.push(name)
return '([^/]+)'
})
const match = path.match(new RegExp(`^${regexStr}$`))
if (match) {
const params = {}
paramNames.forEach((name, i) => { params[name] = match[i + 1] })
return { handler, params }
}
}
return null
}
async navigate(path) {
// Запускаем гварды
for (const guard of this._guards) {
const result = await guard(this.getCurrentPath(), path)
if (result === false) {
console.log(`[Router] Навигация заблокирована гвардом: ${path}`)
return false
}
}
// Обрезаем «будущую» историю при новом переходе
this._history = this._history.slice(0, this._historyIndex + 1)
this._history.push(path)
this._historyIndex++
const match = this._matchRoute(path)
const result = match ? match.handler(match.params) : '404 Страница не найдена'
console.log(`[Router] Переход: ${path}`)
console.log(`[Router] Контент: ${result}`)
this._listeners.forEach(fn => fn(path, result))
return result
}
back() {
if (this._historyIndex <= 0) {
console.log('[Router] Некуда идти назад')
return null
}
this._historyIndex--
const path = this._history[this._historyIndex]
const match = this._matchRoute(path)
const result = match ? match.handler(match.params) : '404'
console.log(`[Router] Назад → ${path}`)
this._listeners.forEach(fn => fn(path, result))
return result
}
forward() {
if (this._historyIndex >= this._history.length - 1) {
console.log('[Router] Некуда идти вперёд')
return null
}
this._historyIndex++
const path = this._history[this._historyIndex]
const match = this._matchRoute(path)
const result = match ? match.handler(match.params) : '404'
console.log(`[Router] Вперёд → ${path}`)
this._listeners.forEach(fn => fn(path, result))
return result
}
getCurrentPath() {
return this._history[this._historyIndex]
}
onChange(listener) {
this._listeners.push(listener)
}
}
// Конфигурация роутера
const router = new MiniRouter()
router
.route('/', () => 'Главная страница')
.route('/about', () => 'О компании')
.route('/users', () => 'Список пользователей')
.route('/users/:id', ({ id }) => `Профиль пользователя #${id}`)
.route('/users/:id/posts/:postId', ({ id, postId }) =>
`Пост #${postId} пользователя #${id}`
)
// Гвард: защита admin-страниц
router.addGuard((from, to) => {
if (to.startsWith('/admin')) {
console.log('[Guard] Доступ к /admin запрещён')
return false
}
return true
})
// Подписываемся на изменения
router.onChange((path, content) => {
console.log(`[App] Рендерим: ${content}`)
})
// Навигация
await router.navigate('/')
await router.navigate('/about')
await router.navigate('/users/42')
await router.navigate('/users/42/posts/7')
await router.navigate('/admin') // заблокирован гвардом
console.log('\n--- Кнопки назад/вперёд ---')
router.back() // /users/42/posts/7
router.back() // /users/42
router.forward() // /users/42/posts/7В традиционных сайтах каждая страница — отдельный HTML-документ. В SPA (Single Page Application) страница одна, а маршрутизация симулируется через JavaScript. History API — это механизм браузера, который позволяет менять URL без перезагрузки страницы.
Два ключевых метода:
// pushState — добавляет новую запись в историю
history.pushState(state, title, url)
// state — объект состояния (до 640 КБ)
// title — устарел, браузеры игнорируют
// url — новый URL (должен быть того же домена)
history.pushState({ page: 'about' }, '', '/about')
// URL изменился на /about, страница НЕ перезагрузилась
// replaceState — заменяет текущую запись (не добавляет в историю)
history.replaceState({ page: 'home' }, '', '/')history.back() // кнопка Назад
history.forward() // кнопка Вперёд
history.go(-2) // на 2 шага назад
history.go(1) // на 1 шаг вперёд
history.go(0) // перезагрузить страницуСрабатывает когда пользователь нажимает Назад/Вперёд или вызывается history.go(). НЕ срабатывает на pushState/replaceState:
window.addEventListener('popstate', (event) => {
console.log('Переход:', location.pathname)
console.log('Состояние:', event.state)
renderPage(location.pathname)
})Hash-роутинг: example.com/#/about
# не отправляется на серверhashchangeHistory-роутинг: example.com/about
#popstateconst routes = {
'/': () => '<h1>Главная</h1>',
'/about': () => '<h1>О нас</h1>',
'/users/:id': ({ id }) => `<h1>Пользователь ${id}</h1>`,
}
function navigate(path) {
history.pushState({}, '', path)
render(path)
}
function render(path) {
const handler = matchRoute(path, routes)
document.getElementById('app').innerHTML = handler ? handler() : '404'
}
window.addEventListener('popstate', () => render(location.pathname))pushState может сохранять объект состояния (до 640 КБ), который возвращается в event.state при popstate:
history.pushState({ scrollY: 450, filters: ['js', 'python'] }, '', '/articles')
window.addEventListener('popstate', (e) => {
if (e.state?.scrollY) window.scrollTo(0, e.state.scrollY)
})Перед переходом можно проверить условия (например, несохранённые изменения):
function guardedNavigate(path) {
if (hasUnsavedChanges() && !confirm('Есть несохранённые изменения. Уйти?')) {
return false
}
navigate(path)
return true
}React Router и Vue Router используют History API под капотом. useNavigate() в React Router вызывает history.pushState. <router-link> в Vue Router делает то же самое.
Мини-роутер с матчингом маршрутов, гвардами навигации и симуляцией истории браузера
// Мини-роутер с поддержкой параметров и гвардов
class MiniRouter {
constructor() {
this._routes = new Map()
this._history = ['/']
this._historyIndex = 0
this._guards = []
this._listeners = []
}
// Регистрация маршрута (поддержка параметров :param)
route(pattern, handler) {
this._routes.set(pattern, handler)
return this
}
// Гвард навигации — может отменить переход
addGuard(guardFn) {
this._guards.push(guardFn)
}
// Сопоставление URL с паттерном, извлечение параметров
_matchRoute(path) {
for (const [pattern, handler] of this._routes) {
const paramNames = []
const regexStr = pattern.replace(/:([^/]+)/g, (_, name) => {
paramNames.push(name)
return '([^/]+)'
})
const match = path.match(new RegExp(`^${regexStr}$`))
if (match) {
const params = {}
paramNames.forEach((name, i) => { params[name] = match[i + 1] })
return { handler, params }
}
}
return null
}
async navigate(path) {
// Запускаем гварды
for (const guard of this._guards) {
const result = await guard(this.getCurrentPath(), path)
if (result === false) {
console.log(`[Router] Навигация заблокирована гвардом: ${path}`)
return false
}
}
// Обрезаем «будущую» историю при новом переходе
this._history = this._history.slice(0, this._historyIndex + 1)
this._history.push(path)
this._historyIndex++
const match = this._matchRoute(path)
const result = match ? match.handler(match.params) : '404 Страница не найдена'
console.log(`[Router] Переход: ${path}`)
console.log(`[Router] Контент: ${result}`)
this._listeners.forEach(fn => fn(path, result))
return result
}
back() {
if (this._historyIndex <= 0) {
console.log('[Router] Некуда идти назад')
return null
}
this._historyIndex--
const path = this._history[this._historyIndex]
const match = this._matchRoute(path)
const result = match ? match.handler(match.params) : '404'
console.log(`[Router] Назад → ${path}`)
this._listeners.forEach(fn => fn(path, result))
return result
}
forward() {
if (this._historyIndex >= this._history.length - 1) {
console.log('[Router] Некуда идти вперёд')
return null
}
this._historyIndex++
const path = this._history[this._historyIndex]
const match = this._matchRoute(path)
const result = match ? match.handler(match.params) : '404'
console.log(`[Router] Вперёд → ${path}`)
this._listeners.forEach(fn => fn(path, result))
return result
}
getCurrentPath() {
return this._history[this._historyIndex]
}
onChange(listener) {
this._listeners.push(listener)
}
}
// Конфигурация роутера
const router = new MiniRouter()
router
.route('/', () => 'Главная страница')
.route('/about', () => 'О компании')
.route('/users', () => 'Список пользователей')
.route('/users/:id', ({ id }) => `Профиль пользователя #${id}`)
.route('/users/:id/posts/:postId', ({ id, postId }) =>
`Пост #${postId} пользователя #${id}`
)
// Гвард: защита admin-страниц
router.addGuard((from, to) => {
if (to.startsWith('/admin')) {
console.log('[Guard] Доступ к /admin запрещён')
return false
}
return true
})
// Подписываемся на изменения
router.onChange((path, content) => {
console.log(`[App] Рендерим: ${content}`)
})
// Навигация
await router.navigate('/')
await router.navigate('/about')
await router.navigate('/users/42')
await router.navigate('/users/42/posts/7')
await router.navigate('/admin') // заблокирован гвардом
console.log('\n--- Кнопки назад/вперёд ---')
router.back() // /users/42/posts/7
router.back() // /users/42
router.forward() // /users/42/posts/7Реализуй createRouter(routes) где routes — объект вида { "/path": handlerFn }. Методы: navigate(path) вызывает нужный обработчик и добавляет в историю, back() возвращается назад, forward() идёт вперёд, getCurrentPath() возвращает текущий путь, onNavigate(callback) регистрирует обработчик переходов. История симулируется через массив.
history.splice(currentIndex + 1) обрежет всё после текущего индекса. После navigate() currentIndex = history.length - 1. back() делает currentIndex-- перед рендером. forward() делает currentIndex++. getCurrentPath() возвращает history[currentIndex].