⏱ ~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, Без залежностей.