← JavaScript/localStorage и sessionStorage#89 из 383← ПредыдущийСледующий →+25 XP
Полезно по теме:Гайд: как учить JavaScriptПрактика: JS базаПрактика: async и сетьТермин: Closure

localStorage и sessionStorage

Какую проблему решает Web Storage

Ты заходишь на Netflix — сайт помнит, что ты выбрал тёмную тему и язык «Русский». Ты закрываешь Notion и открываешь снова — черновик твоей заметки всё ещё там. Всё это без авторизации, без запросов к серверу. Web Storage API позволяет хранить данные прямо в браузере пользователя.

На основе предыдущих уроков

  • «JSON» — объекты нужно сериализовать перед сохранением (JSON.stringify/parse)
  • «try/catch» — JSON.parse и Storage могут бросать ошибки
  • «Функции» — паттерн useLocalState возвращает getter/setter функции
  • localStorage vs sessionStorage

    | | localStorage | sessionStorage |

    |---|---|---|

    | Время жизни | До явного удаления | До закрытия вкладки |

    | Вкладки | Общий для всех вкладок | Только текущая вкладка |

    | Объём | ~5-10 МБ | ~5 МБ |

    | Использование | Настройки, тема, токен | Корзина, форма, сессия |

    Базовый API

    // Запись
    localStorage.setItem('theme', 'dark')
    
    // Чтение (null если ключа нет)
    const theme = localStorage.getItem('theme')  // 'dark' или null
    
    // Удаление
    localStorage.removeItem('theme')
    
    // Полная очистка хранилища
    localStorage.clear()

    Хранение объектов — JSON обязателен

    localStorage хранит только строки. Для объектов и массивов используй JSON:

    const userSettings = {
      theme: 'dark',
      language: 'ru',
      notifications: true,
    }
    
    // Сохранение
    localStorage.setItem('settings', JSON.stringify(userSettings))
    
    // Загрузка — безопасно через try/catch и null-проверку
    function loadSettings() {
      const raw = localStorage.getItem('settings')
      if (!raw) return { theme: 'light', language: 'ru', notifications: true }  // дефолт
      try {
        return JSON.parse(raw)
      } catch {
        return {}  // повреждённые данные — возвращаем дефолт
      }
    }

    Паттерны использования

    Тема приложения:

    function setTheme(theme) {
      localStorage.setItem('theme', theme)
      document.body.dataset.theme = theme  // применяем немедленно
    }
    
    // При загрузке страницы — восстанавливаем
    const savedTheme = localStorage.getItem('theme') ?? 'light'
    document.body.dataset.theme = savedTheme

    Токен авторизации:

    // После успешного входа
    function login(token, user) {
      localStorage.setItem('authToken', token)
      localStorage.setItem('user', JSON.stringify(user))
    }
    
    // При каждом API-запросе
    const token = localStorage.getItem('authToken')
    if (token) {
      headers['Authorization'] = 'Bearer ' + token
    }
    
    // Выход
    function logout() {
      localStorage.removeItem('authToken')
      localStorage.removeItem('user')
    }

    Storage-событие — синхронизация между вкладками:

    // Когда одна вкладка меняет localStorage — другие получают событие
    window.addEventListener('storage', (e) => {
      if (e.key === 'theme') {
        document.body.dataset.theme = e.newValue
      }
    })

    Типичные ошибки

    1. Забыли JSON.parse при чтении объекта:

    // Сломано — достаём строку, а не объект:
    localStorage.setItem('user', JSON.stringify({ name: 'Иван', age: 25 }))
    const user = localStorage.getItem('user')  // '{"name":"Иван","age":25}' — строка!
    console.log(user.name)  // undefined
    
    // Исправлено:
    const user = JSON.parse(localStorage.getItem('user'))
    console.log(user.name)  // 'Иван'

    2. Не обработали null при отсутствии ключа:

    // Сломано — getItem возвращает null, JSON.parse(null) = null:
    const settings = JSON.parse(localStorage.getItem('settings'))  // null если нет
    settings.theme  // TypeError: Cannot read properties of null
    
    // Исправлено — проверка на null:
    const raw = localStorage.getItem('settings')
    const settings = raw ? JSON.parse(raw) : { theme: 'light' }

    3. Хранят чувствительные данные:

    // Никогда не храни в localStorage:
    localStorage.setItem('password', userPassword)    // плохо — XSS уязвимость!
    localStorage.setItem('creditCard', cardNumber)   // плохо — читается скриптами!
    
    // Для чувствительных данных — только сервер + httpOnly cookies

    Ограничения localStorage

  • Синхронный API — блокирует поток (не использовать для больших данных)
  • Только строки — нужен JSON для объектов
  • ~5 МБ лимит — не подходит для файлов
  • XSS-уязвим — если на сайте XSS, злоумышленник прочитает всё хранилище
  • Недоступен в воркерах и серверном коде (Node.js)
  • В реальных проектах

  • Тема/язык: GitHub, Twitter, Notion — тема сохраняется без авторизации
  • JWT-токены: многие SPA хранят токены (хотя httpOnly cookies безопаснее)
  • Корзина гостя: Amazon сохраняет товары без входа в аккаунт
  • Draft-режим: Notion, Google Docs — автосохранение черновиков
  • Примеры

    Класс StorageService с namespacing и автосериализацией

    // Симуляция localStorage для sandbox
    const mockStorage = new Map()
    const localStorage = {
      setItem: (k, v) => mockStorage.set(k, String(v)),
      getItem: (k) => mockStorage.get(k) ?? null,
      removeItem: (k) => mockStorage.delete(k),
      clear: () => mockStorage.clear(),
      get length() { return mockStorage.size },
      key: (i) => [...mockStorage.keys()][i] ?? null,
    }
    
    // Сервис хранилища с пространством имён
    class StorageService {
      constructor(namespace) {
        this.ns = namespace
      }
    
      _key(key) { return `${this.ns}:${key}` }
    
      set(key, value) {
        try {
          localStorage.setItem(this._key(key), JSON.stringify(value))
          return true
        } catch (e) {
          console.error('StorageService.set ошибка:', e.message)
          return false
        }
      }
    
      get(key, defaultValue = null) {
        const raw = localStorage.getItem(this._key(key))
        if (raw === null) return defaultValue
        try {
          return JSON.parse(raw)
        } catch {
          return defaultValue
        }
      }
    
      remove(key) {
        localStorage.removeItem(this._key(key))
      }
    
      // Очищаем только ключи нашего namespace
      clear() {
        const prefix = this.ns + ':'
        for (let i = localStorage.length - 1; i >= 0; i--) {
          const key = localStorage.key(i)
          if (key?.startsWith(prefix)) localStorage.removeItem(key)
        }
      }
    }
    
    // Хранилища для разных частей приложения
    const userStorage = new StorageService('user')
    const appStorage = new StorageService('app')
    
    // Сохранение данных пользователя
    userStorage.set('profile', { name: 'Иван Петров', email: 'ivan@mail.ru', age: 28 })
    userStorage.set('preferences', { theme: 'dark', language: 'ru', timezone: 'Europe/Moscow' })
    
    // Сохранение состояния приложения
    appStorage.set('lastRoute', '/dashboard/analytics')
    appStorage.set('sidebarCollapsed', true)
    
    // Загрузка
    const profile = userStorage.get('profile')
    console.log('Пользователь:', profile.name)          // 'Иван Петров'
    console.log('Email:', profile.email)                // 'ivan@mail.ru'
    
    const prefs = userStorage.get('preferences')
    console.log('Тема:', prefs.theme)                   // 'dark'
    
    const route = appStorage.get('lastRoute', '/')
    console.log('Последняя страница:', route)            // '/dashboard/analytics'
    
    const collapsed = appStorage.get('sidebarCollapsed', false)
    console.log('Сайдбар свёрнут:', collapsed)          // true
    
    // Дефолтное значение если ключа нет
    const missing = userStorage.get('nonexistent', { fallback: true })
    console.log('Дефолт:', missing.fallback)            // true

    localStorage и sessionStorage

    Какую проблему решает Web Storage

    Ты заходишь на Netflix — сайт помнит, что ты выбрал тёмную тему и язык «Русский». Ты закрываешь Notion и открываешь снова — черновик твоей заметки всё ещё там. Всё это без авторизации, без запросов к серверу. Web Storage API позволяет хранить данные прямо в браузере пользователя.

    На основе предыдущих уроков

  • «JSON» — объекты нужно сериализовать перед сохранением (JSON.stringify/parse)
  • «try/catch» — JSON.parse и Storage могут бросать ошибки
  • «Функции» — паттерн useLocalState возвращает getter/setter функции
  • localStorage vs sessionStorage

    | | localStorage | sessionStorage |

    |---|---|---|

    | Время жизни | До явного удаления | До закрытия вкладки |

    | Вкладки | Общий для всех вкладок | Только текущая вкладка |

    | Объём | ~5-10 МБ | ~5 МБ |

    | Использование | Настройки, тема, токен | Корзина, форма, сессия |

    Базовый API

    // Запись
    localStorage.setItem('theme', 'dark')
    
    // Чтение (null если ключа нет)
    const theme = localStorage.getItem('theme')  // 'dark' или null
    
    // Удаление
    localStorage.removeItem('theme')
    
    // Полная очистка хранилища
    localStorage.clear()

    Хранение объектов — JSON обязателен

    localStorage хранит только строки. Для объектов и массивов используй JSON:

    const userSettings = {
      theme: 'dark',
      language: 'ru',
      notifications: true,
    }
    
    // Сохранение
    localStorage.setItem('settings', JSON.stringify(userSettings))
    
    // Загрузка — безопасно через try/catch и null-проверку
    function loadSettings() {
      const raw = localStorage.getItem('settings')
      if (!raw) return { theme: 'light', language: 'ru', notifications: true }  // дефолт
      try {
        return JSON.parse(raw)
      } catch {
        return {}  // повреждённые данные — возвращаем дефолт
      }
    }

    Паттерны использования

    Тема приложения:

    function setTheme(theme) {
      localStorage.setItem('theme', theme)
      document.body.dataset.theme = theme  // применяем немедленно
    }
    
    // При загрузке страницы — восстанавливаем
    const savedTheme = localStorage.getItem('theme') ?? 'light'
    document.body.dataset.theme = savedTheme

    Токен авторизации:

    // После успешного входа
    function login(token, user) {
      localStorage.setItem('authToken', token)
      localStorage.setItem('user', JSON.stringify(user))
    }
    
    // При каждом API-запросе
    const token = localStorage.getItem('authToken')
    if (token) {
      headers['Authorization'] = 'Bearer ' + token
    }
    
    // Выход
    function logout() {
      localStorage.removeItem('authToken')
      localStorage.removeItem('user')
    }

    Storage-событие — синхронизация между вкладками:

    // Когда одна вкладка меняет localStorage — другие получают событие
    window.addEventListener('storage', (e) => {
      if (e.key === 'theme') {
        document.body.dataset.theme = e.newValue
      }
    })

    Типичные ошибки

    1. Забыли JSON.parse при чтении объекта:

    // Сломано — достаём строку, а не объект:
    localStorage.setItem('user', JSON.stringify({ name: 'Иван', age: 25 }))
    const user = localStorage.getItem('user')  // '{"name":"Иван","age":25}' — строка!
    console.log(user.name)  // undefined
    
    // Исправлено:
    const user = JSON.parse(localStorage.getItem('user'))
    console.log(user.name)  // 'Иван'

    2. Не обработали null при отсутствии ключа:

    // Сломано — getItem возвращает null, JSON.parse(null) = null:
    const settings = JSON.parse(localStorage.getItem('settings'))  // null если нет
    settings.theme  // TypeError: Cannot read properties of null
    
    // Исправлено — проверка на null:
    const raw = localStorage.getItem('settings')
    const settings = raw ? JSON.parse(raw) : { theme: 'light' }

    3. Хранят чувствительные данные:

    // Никогда не храни в localStorage:
    localStorage.setItem('password', userPassword)    // плохо — XSS уязвимость!
    localStorage.setItem('creditCard', cardNumber)   // плохо — читается скриптами!
    
    // Для чувствительных данных — только сервер + httpOnly cookies

    Ограничения localStorage

  • Синхронный API — блокирует поток (не использовать для больших данных)
  • Только строки — нужен JSON для объектов
  • ~5 МБ лимит — не подходит для файлов
  • XSS-уязвим — если на сайте XSS, злоумышленник прочитает всё хранилище
  • Недоступен в воркерах и серверном коде (Node.js)
  • В реальных проектах

  • Тема/язык: GitHub, Twitter, Notion — тема сохраняется без авторизации
  • JWT-токены: многие SPA хранят токены (хотя httpOnly cookies безопаснее)
  • Корзина гостя: Amazon сохраняет товары без входа в аккаунт
  • Draft-режим: Notion, Google Docs — автосохранение черновиков
  • Примеры

    Класс StorageService с namespacing и автосериализацией

    // Симуляция localStorage для sandbox
    const mockStorage = new Map()
    const localStorage = {
      setItem: (k, v) => mockStorage.set(k, String(v)),
      getItem: (k) => mockStorage.get(k) ?? null,
      removeItem: (k) => mockStorage.delete(k),
      clear: () => mockStorage.clear(),
      get length() { return mockStorage.size },
      key: (i) => [...mockStorage.keys()][i] ?? null,
    }
    
    // Сервис хранилища с пространством имён
    class StorageService {
      constructor(namespace) {
        this.ns = namespace
      }
    
      _key(key) { return `${this.ns}:${key}` }
    
      set(key, value) {
        try {
          localStorage.setItem(this._key(key), JSON.stringify(value))
          return true
        } catch (e) {
          console.error('StorageService.set ошибка:', e.message)
          return false
        }
      }
    
      get(key, defaultValue = null) {
        const raw = localStorage.getItem(this._key(key))
        if (raw === null) return defaultValue
        try {
          return JSON.parse(raw)
        } catch {
          return defaultValue
        }
      }
    
      remove(key) {
        localStorage.removeItem(this._key(key))
      }
    
      // Очищаем только ключи нашего namespace
      clear() {
        const prefix = this.ns + ':'
        for (let i = localStorage.length - 1; i >= 0; i--) {
          const key = localStorage.key(i)
          if (key?.startsWith(prefix)) localStorage.removeItem(key)
        }
      }
    }
    
    // Хранилища для разных частей приложения
    const userStorage = new StorageService('user')
    const appStorage = new StorageService('app')
    
    // Сохранение данных пользователя
    userStorage.set('profile', { name: 'Иван Петров', email: 'ivan@mail.ru', age: 28 })
    userStorage.set('preferences', { theme: 'dark', language: 'ru', timezone: 'Europe/Moscow' })
    
    // Сохранение состояния приложения
    appStorage.set('lastRoute', '/dashboard/analytics')
    appStorage.set('sidebarCollapsed', true)
    
    // Загрузка
    const profile = userStorage.get('profile')
    console.log('Пользователь:', profile.name)          // 'Иван Петров'
    console.log('Email:', profile.email)                // 'ivan@mail.ru'
    
    const prefs = userStorage.get('preferences')
    console.log('Тема:', prefs.theme)                   // 'dark'
    
    const route = appStorage.get('lastRoute', '/')
    console.log('Последняя страница:', route)            // '/dashboard/analytics'
    
    const collapsed = appStorage.get('sidebarCollapsed', false)
    console.log('Сайдбар свёрнут:', collapsed)          // true
    
    // Дефолтное значение если ключа нет
    const missing = userStorage.get('nonexistent', { fallback: true })
    console.log('Дефолт:', missing.fallback)            // true

    Задание

    Ты разрабатываешь менеджер настроек для веб-приложения. Используй мок-localStorage (уже написан). Реализуй функцию `createSettingsStore(defaults)`, которая возвращает объект с методами: - `get(key)` — возвращает значение из хранилища или дефолт - `set(key, value)` — сохраняет значение - `reset()` — сбрасывает все настройки к дефолтам Покажи сохранение и восстановление темы, языка и других настроек.

    Подсказка

    get: return JSON.parse(raw). set: fakeLocalStorage.setItem(PREFIX + key, JSON.stringify(value)). reset: Object.entries(defaults).forEach(([key, value]) => this.set(key, value)).

    Загружаем среду выполнения...
    Загружаем AI-помощника...