JavaScript · Веб-API
⏱ ~35 хв🟢 Початковий рівеньЧистий JS · Без залежностей

Збереження налаштувань симуляції через localStorage + JSON

Зберігайте значення повзунків, перемикачі, кути камери та кількість частинок між перезавантаженнями сторінки без жодного бекенду — за допомогою localStorage, JSON.stringify, версіонування схеми та IndexedDB для більших даних.

1

Основи читання/запису localStorage

localStorage — це синхронне сховище ключ-значення обсягом до ~5–10 МБ на джерело (залежить від браузера). Значення мають бути рядками, тож для структурованих даних використовуйте JSON.

// Запис — завжди серіалізуйте обʼєкти localStorage.setItem('gravity', '9.81'); localStorage.setItem('simState', JSON.stringify({ particles: 500, gravity: 9.81, paused: false })); // Читання — парсимо назад до початкового типу const g = parseFloat(localStorage.getItem('gravity') ?? '9.81'); const st = JSON.parse(localStorage.getItem('simState') ?? 'null'); // Видалення одного ключа localStorage.removeItem('gravity'); // Видалення ВСІХ ключів цього джерела — обережно! // localStorage.clear(); // Перебираємо всі збережені ключі for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); console.log(key, localStorage.getItem(key)); }
localStorage синхронний і блокує головний потік. Для читання та невеликих записів (< 1 КБ) це нормально. Ніколи не зберігайте двійкові дані чи великі буфери — натомість використовуйте IndexedDB (Крок 6).
2

Збереження всіх параметрів симуляції за раз

Замість зберігання окремих ключів зберіть весь стан симуляції в один обʼєкт-знімок і запишіть його під єдиним ключем. Це робить збереження та завантаження атомарними.

// Стан симуляції — усі параметри в одному обʼєкті const DEFAULT_STATE = { particleCount: 500, gravity: -9.81, friction: 0.98, paused: false, colorMode: 'velocity', // 'velocity' | 'density' | 'uniform' camera: { x: 0, y: 5, z: 15, pitch: 0, yaw: 0 }, }; let state = { ...DEFAULT_STATE }; // --- Збереження --- function saveState() { try { localStorage.setItem('boids-state', JSON.stringify(state)); } catch (e) { handleQuotaError(e); // Крок 5 } } // Викликайте saveState() щоразу, коли користувач змінює параметр: document.getElementById('gravity-slider').addEventListener('input', e => { state.gravity = parseFloat(e.target.value); saveState(); }); // Або зберігайте на window beforeunload (ловить закриття вкладки) window.addEventListener('beforeunload', saveState);
3

Автовідновлення стану під час завантаження сторінки

Під час запуску завантажте збережений стан і застосуйте його до кожного елемента інтерфейсу перед ініціалізацією симуляції. Якщо нічого не збережено, відкотіться до значень за замовчуванням.

function loadState() { const raw = localStorage.getItem('boids-state'); if (!raw) return DEFAULT_STATE; try { const saved = JSON.parse(raw); // Обʼєднуємо зі значеннями за замовчуванням, щоб нові ключі з пізніших версій усе ще існували return Object.assign({}, DEFAULT_STATE, saved); } catch { console.warn('Corrupt state, resetting'); localStorage.removeItem('boids-state'); return DEFAULT_STATE; } } // Застосовуємо стан до елементів керування DOM function applyStateToUI(st) { document.getElementById('particle-count').value = st.particleCount; document.getElementById('gravity-slider').value = st.gravity; document.getElementById('friction-slider').value = st.friction; document.getElementById('paused-toggle').checked = st.paused; document.getElementById('color-mode').value = st.colorMode; } // --- Послідовність запуску --- state = loadState(); applyStateToUI(state); initSimulation(state); // передаємо збережений стан в ініціалізацію вашої симуляції
Завжди обʼєднуйте зі значеннями за замовчуванням (Object.assign({}, DEFAULT_STATE, saved)). Якщо користувач оновить сторінку й до значень за замовчуванням додасться нове поле, обʼєднання гарантує, що він отримає нове типове значення замість undefined.
4

Версіонування схеми та міграція

Коли ви змінюєте структуру обʼєкта стану (перейменовуєте ключ, змінюєте одиницю виміру), старі збережені дані стають несумісними. Додайте номер версії _v і мігруйте старі формати.

const SCHEMA_VERSION = 3; // підвищуйте щоразу, коли схема стану змінюється function loadStateVersioned() { const raw = localStorage.getItem('boids-state'); if (!raw) return { ...DEFAULT_STATE, _v: SCHEMA_VERSION }; let saved; try { saved = JSON.parse(raw); } catch { return { ...DEFAULT_STATE, _v: SCHEMA_VERSION }; } // Мігруємо зі старіших версій const v = saved._v ?? 1; if (v < 2) { // v1→v2: перейменували 'speed' на 'gravity', змінили угоду про знак saved.gravity = -(saved.speed ?? 9.81); delete saved.speed; } if (v < 3) { // v2→v3: камера перейшла з плоских ключів до вкладеного обʼєкта saved.camera = { x: saved.camX ?? 0, y: saved.camY ?? 5, z: saved.camZ ?? 15, pitch: 0, yaw: 0 }; delete saved.camX; delete saved.camY; delete saved.camZ; } saved._v = SCHEMA_VERSION; return Object.assign({}, DEFAULT_STATE, saved); }
Тримайте міграції адитивними — лише перетворюйте старі поля, ніколи не видаляйте дані користувача мовчки. Якщо міграція деструктивна (наприклад, одиниці змінилися з кг на г), покажіть одноразовий банер: «Налаштування оновлено відповідно до нового формату».
5

Помилки квоти та коректне резервне поводження

Safari обмежує localStorage до 5 МБ, Firefox/Chrome — до 10 МБ. Коли сховище заповнене, setItem кидає QuotaExceededError. Завжди обгортайте записи в try/catch.

function handleQuotaError(error) { // Коди QuotaExceededError відрізняються залежно від браузера const isQuota = error.name === 'QuotaExceededError' || error.name === 'NS_ERROR_DOM_QUOTA_REACHED' || error.code === 22 || error.code === 1014; if (isQuota) { // Стратегія 1: видаляємо старі/великі ключі const KEEP = ['boids-state', 'user-prefs']; for (let i = localStorage.length - 1; i >= 0; i--) { const key = localStorage.key(i); if (!KEEP.includes(key)) localStorage.removeItem(key); } // Пробуємо ще раз try { localStorage.setItem('boids-state', JSON.stringify(state)); } catch { /* здаємося */ } return; } // Недоступно (приватний перегляд, сховище вимкнене) console.warn('localStorage unavailable:', error.message); } // Перевіряємо доступність localStorage під час запуску function storageAvailable() { try { localStorage.setItem('__test__', '1'); localStorage.removeItem('__test__'); return true; } catch { return false; } } const CAN_PERSIST = storageAvailable();
У приватному режимі Safari localStorage існує, але одразу кидає помилку на будь-якому записі (квота = 0). Завжди виконуйте перевірку доступності під час запуску й показуйте індикатор «налаштування не збережено», якщо вона не вдається.
6

IndexedDB для великих даних

Для даних більших за кілька КБ — просторові хеш-таблиці, попередньо обчислені текстури пошуку, великі знімки частинок — використовуйте IndexedDB. Це асинхронне сховище, що підтримує двійкові дані й не має практичного обмеження за розміром понад кілька сотень МБ.

// Мінімальний помічник для IndexedDB (async/await) function openDB(name, version, upgrade) { return new Promise((resolve, reject) => { const req = indexedDB.open(name, version); req.onupgradeneeded = e => upgrade(e.target.result); req.onsuccess = e => resolve(e.target.result); req.onerror = e => reject(e.target.error); }); } const txGet = (db, store, key) => new Promise((res, rej) => { const tx = db.transaction(store, 'readonly'); const req = tx.objectStore(store).get(key); req.onsuccess = () => res(req.result); req.onerror = () => rej(req.error); }); const txPut = (db, store, key, value) => new Promise((res, rej) => { const tx = db.transaction(store, 'readwrite'); const req = tx.objectStore(store).put(value, key); req.onsuccess = () => res(); req.onerror = () => rej(req.error); }); // --- Використання --- const db = await openDB('sim-cache', 1, db => { db.createObjectStore('blobs'); // зберігає будь-яке значення за рядковим ключем }); // Зберігаємо великий Float32Array (знімок поля густини) const snapshot = new Float32Array(512 * 512); // ... заповнюємо snapshot ... await txPut(db, 'blobs', 'density-field', snapshot); // Завантажуємо назад const restored = await txGet(db, 'blobs', 'density-field'); // restored — той самий Float32Array
Коли що використовувати:
localStorage → невеликий JSON-стан (< 10 КБ), потрібен синхронний доступ, прості ключ/значення.
sessionStorage → те саме, що localStorage, але очищується при закритті вкладки — добре для тимчасового стану.
IndexedDB → типізовані масиви, блоби, великі таблиці пошуку, кеш текстур, будь-що понад ~50 КБ.

Часті запитання

Чого я навчуся в цьому уроці?

Зберігайте повзунки, прапорці та стан камери симуляції між перезавантаженнями сторінки за допомогою localStorage та JSON — з версіонуванням, обробкою квоти й IndexedDB для великих даних.

Які теми розглядаються в цьому уроці?

Цей урок охоплює такі теми: Основи читання/запису localStorage, Збереження всіх параметрів симуляції за раз, Автовідновлення стану під час завантаження сторінки, Версіонування схеми та міграція, Помилки квоти та коректне резервне поводження, IndexedDB для великих даних.

Які інструменти й технології використовуються?

Цей урок використовує: ⏱ ~35 хв 🟢 Початковий рівень Чистий JS, Без залежностей.

Скільки часу займає цей урок?

Цей урок займає приблизно 35 хв.