JavaScript и viewport: инструменты адаптивного дизайна
Определение viewport в JavaScript
Как получить точный размер видимой области с учётом мобильных браузеров, когда адресная строка или панель инструментов меняют высоту?
Основное решение: использовать window.visualViewport – современный API, предоставляющий актуальные размеры видимой области (включая динамические изменения, вызванные появлением клавиатуры или скроллом браузера).
// Получение размеров через visualViewport
if (window.visualViewport) {
const width = window.visualViewport.width;
const height = window.visualViewport.height;
console.log(`Ширина: ${width}px, Высота: ${height}px`);
}
Viewport js (viewport в javascript)
API поддерживается во всех современных браузерах, включая Safari на iOS. Событие resize на объекте visualViewport позволяет отслеживать изменения в реальном времени.
Цель: получить корректную высоту при открытии клавиатуры на мобильных устройствах или при изменении видимой области из-за панелей навигации.
Типичная ошибка: полагаться на window.innerHeight в мобильных браузерах, так как оно может включать высоту адресной строки. Решение: всегда использовать window.visualViewport для чувствительных к размерам элементов.
Проблема: метод не работает в старых браузерах (Internet Explorer). Для них применять window.innerHeight как запасной вариант.
Варианты получения размеров viewport
Как получить размер области просмотра без учёта панелей инструментов браузера (только видимое содержимое)?
Использовать window.innerWidth и window.innerHeight. Эти значения возвращают ширину и высоту внутреннего окна, включая полосу прокрутки (если она есть).
console.log(`inner: ${window.innerWidth}x${window.innerHeight}`);
Цель: когда требуется получить размер всего окна, включая возможные скрытые панели (работает в большинстве настольных браузеров).
На мобильных устройствах в Safari эта пара может не учитывать изменения при появлении клавиатуры, что приводит к некорректной высоте.
Как получить размер видимого содержимого страницы (без полос прокрутки и панелей браузера)?
Использовать document.documentElement.clientWidth и document.documentElement.clientHeight. Эти значения показывают размер области, где размещается контент (за вычетом полосы прокрутки).
console.log(`client: ${document.documentElement.clientWidth}x${document.documentElement.clientHeight}`);
Цель: получить размеры контейнера, не включающего полосы прокрутки, например, для расчёта высоты футера или полноэкранных блоков.
В некоторых версиях браузеров высота может отличаться от window.innerHeight из-за панели закладок или инструментов разработчика.
Как реагировать на изменения viewport с помощью CSS-медиа-запросов в JavaScript?
Использовать window.matchMedia. Создаётся объект, представляющий медиа-запрос, и добавляется слушатель изменения.
const mq = window.matchMedia('(min-width: 768px)');
function handleMQ(e) {
if (e.matches) console.log('Ширина больше 768px');
else console.log('Ширина меньше 768px');
}
mq.addListener(handleMQ);
handleMQ(mq);
Цель: динамически менять логику приложения в зависимости от контрольных точек адаптивности, избегая множественных проверок innerWidth.
Не все старые браузеры поддерживают addListener (устаревший синтаксис). Рекомендуется использовать addEventListener с современным синтаксисом: mq.addEventListener('change', handleMQ).
Как отслеживать изменения размеров viewport с помощью ResizeObserver?
ResizeObserver может наблюдать за document.documentElement или window. При каждом изменении размера вызывается колбэк.
const observer = new ResizeObserver(entries => {
for (const entry of entries) {
console.log('Новый размер:', entry.contentRect.width, entry.contentRect.height);
}
});
observer.observe(document.documentElement);
Цель: более эффективное отслеживание изменений по сравнению с событием resize (меньше накладных расходов) и возможность наблюдать за конкретными элементами.
Поддержка ResizeObserver в большинстве современных браузеров хорошая, но в Internet Explorer её нет. Также наблюдатель может вызывать колбэк при изменении дочерних элементов, что иногда излишне.
Как обрабатывать изменение размеров окна с помощью события resize?
Классический подход – слушать событие resize на объекте window. Для оптимизации используют throttle или debounce.
window.addEventListener('resize', function() {
console.log('Новый размер:', window.innerWidth, window.innerHeight);
});
Цель: простой способ реакции на изменение окна, подходит для настольных устройств с небольшой частотой событий.
Событие может вызываться десятки раз в секунду, что приводит к снижению производительности. Решение: использовать debounce (например, setTimeout) или requestAnimationFrame.
На мобильных устройствах событие может не срабатывать при открытии клавиатуры; для этого лучше применять visualViewport.
Расширенные примеры работы с viewport в JavaScript
Пример 1: Реальное время с visualViewport и адаптация высоты под клавиатуру
// Выводим размеры видимой области и устанавливаем CSS-переменную
function updateViewport() {
if (window.visualViewport) {
const { width, height } = window.visualViewport;
document.documentElement.style.setProperty('--vh', `${height * 0.01}px`);
document.getElementById('sizeOutput').textContent = `${width} × ${height}`;
}
}
window.visualViewport?.addEventListener('resize', updateViewport);
updateViewport();
Результат: CSS-переменная --vh равна 1% от высоты видимой области; при появлении клавиатуры высота уменьшается, и элементы с высотой 100vh (через calc(100 * var(--vh) + 0px)) корректно адаптируются.
Пример 2: Сравнение всех методов для отладки на разных устройствах
function logSizes() {
console.log('inner', window.innerWidth, window.innerHeight);
console.log('client', document.documentElement.clientWidth, document.documentElement.clientHeight);
if (window.visualViewport) {
console.log('visual', window.visualViewport.width, window.visualViewport.height);
}
}
window.addEventListener('resize', logSizes);
logSizes();
Результат (пример для iPhone с клавиатурой): inner: 375 812 client: 375 646 visual: 375 400
Видно, что visualViewport даёт наименьшую высоту при открытой клавиатуре, что наиболее точно.
Пример 3: Использование matchMedia для переключения между десктопным и мобильным кодом
const isDesktop = window.matchMedia('(min-width: 1024px)');
function toggleAppMode(e) {
if (e.matches) {
loadDesktopScript();
} else {
loadMobileScript();
}
}
isDesktop.addEventListener('change', toggleAppMode);
toggleAppMode(isDesktop);
Результат: при изменении ширины окна автоматически подключаются разные скрипты (например, слайдер для десктопа и свайп-навигация для мобильных).
Пример 4: Коррекция 100vh на мобильных с помощью visualViewport и CSS custom properties
function setVh() {
const vh = window.visualViewport ? window.visualViewport.height * 0.01 : window.innerHeight * 0.01;
document.documentElement.style.setProperty('--vh', `${vh}px`);
}
window.addEventListener('resize', setVh);
// Использование в CSS:
// .fullscreen { height: calc(var(--vh, 1vh) * 100); }
Результат: блоки с 100vh не обрезаются под адресную строку на мобильных и корректно пересчитываются при её скрытии или появлении клавиатуры.
Пример 5: ResizeObserver для определения изменения размера контейнера (а не viewport)
const sidebar = document.getElementById('sidebar');
const observer = new ResizeObserver(entries => {
for (let entry of entries) {
if (entry.contentBoxSize) {
const inlineSize = entry.contentBoxSize[0].inlineSize;
console.log('Ширина сайдбара:', inlineSize);
}
}
});
observer.observe(sidebar);
Результат: при изменении ширины элемента sidebar (например, из-за коллапса) выводится актуальная ширина. Это удобно для адаптивных панелей, размер которых меняется динамически.
Пример 6: Debounced обработчик resize для производительности
function debounce(fn, delay) {
let timer;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
const handler = debounce(() => {
console.log('Окно изменено:', window.innerWidth);
}, 200);
window.addEventListener('resize', handler);
Результат: событие resize обрабатывается не чаще одного раза в 200 мс, снижая нагрузку на рендеринг.