Web Storage API: примеры использования

Доброго времени суток, друзья!

В данной статье мы рассмотрим парочку примеров использования Web Storage API или объекта «Storage».

Что конкретно мы будем делать?

  • Научимся запоминать время воспроизведения видео.
  • Поработаем с формой входа на страницу.
  • Напишем логику списка задач (todo list).
  • Схематично набросаем корзину для товаров.

Итак, поехали.

Краткий обзор

Объект «Storage» используется для хранения данных на стороне клиента и в этом смысле выступает альтернативой cookies. Преимущество Storage состоит в размере хранилища (от 5 Мб, зависит от браузера, при превышении лимита выбрасывается ошибка «QUOTA_EXCEEDED_ERR») и отсутствии необходимости обращаться к серверу. Существенный недостаток — безопасность: стоит вредоносному скрипту получить доступ к странице, и пиши пропало. Поэтому крайне не рекомендуется хранить в Storage конфиденциальную информацию.

Справедливости ради стоит отметить, что на сегодняшний день существуют более продвинутые решения для хранения данных на стороне клиента — это IndexedBD, Service Workers + Cache API и др.

О сервис-воркерах можно почитать здесь.

Web Storage API включает в себя localStorage и sessionStorage. Разница между ними состоит во времени хранения данных. В localStorage данные хранятся постоянно до их «явного» удаления (ни перезагрузка страницы, ни ее закрытие не приводят к удалению данных). Время хранения данных в sessionStorage, как следует из названия, ограничено сессией браузера. Поскольку sessionStorage на практике почти не используется, мы будет рассматривать только localStorage.

Что необходимо знать о localStorage?

  • При использовании localStorage создается представление объекта «Storage».
  • Данные в localStorage хранятся в виде пар ключ/значение.
  • Данные хранятся в виде строк.
  • Данные не сортируются, что иногда приводит к их перемешиванию (в чем мы убедимся на примере списка задач).
  • При включении в браузере режима инкогнито или приватного режима использование localStorage может стать невозможным (зависит от браузера).

localStorage обладает следующими методами:

  • Storage.key(n) — имя ключа с индексом n
  • Storage.getItem() — получить элемент
  • Storage.setItem() — записать элемент
  • Storage.removeItem() — удалить элемент
  • Storage.clear() — очистить хранилище
  • Storage.length — длина хранилища (количество элементов — пар ключ/значение)

В спецификации это выглядит так:

interface Storage {
    readonly attribute unsigned long length;
    DOMString? key(unsigned long index);
    getter DOMString? getItem(DOMString key);
    setter void setItem(DOMString key, DOMString value);
    deleter void removeItem(DOMString key);
    void clear();
};

Данные в хранилище записываются одним из следующих способов:

localStorage.color = 'deepskyblue'
localStorage[color] = 'deepskyblue'
localStorage.setItem('color', 'deepskyblue') // рекомендуется использовать этот способ

Получить данные можно так:

localStorage.getItem('color')
localStorage['color']

Как перебрать ключи хранилища и получить значения?

// способ 1
for (let i = 0; i < localStorage.length; i++) {
    let key = localStorage.key(i)
    console.log(`${key}: ${localStorage.getItem(key)}`)
}

// способ 2
let keys = Object.keys(localStorage)
for (let key of keys) {
    console.log(`${key}: ${localStorage.getItem(key)}`)
}

Как мы отмечали выше, данные в хранилище имеют строковый формат, поэтому с записью объектов возникают некоторые трудности, которые легко решаются с помощью тандема JSON.stringify() — JSON.parse():

localStorage.user = {
    name: 'Harry'
}
console.dir(localStorage.user) // [object Object]

localStorage.user = JSON.stringify({
    name: 'Harry'
})
let user = JSON.parse(localStorage.user)
console.dir(user.name) // Harry

Для взаимодействием с localStorage существует специальное событие — storage (onstorage), которое возникает при записи/удалении данных. Оно имеет следующие свойства:

  • key — ключ
  • oldValue — старое значение
  • newValue — новое значение
  • url — адрес хранилища
  • storageArea — объект, в котором произошло изменение

В спецификации это выглядит так:

[Constructor(DOMString type, optional StorageEventInit eventInitDict)]
    interface StorageEvent : Event {
    readonly attribute DOMString? key;
    readonly attribute DOMString? oldValue;
    readonly attribute DOMString? newValue;
    readonly attribute DOMString url;
    readonly attribute Storage? storageArea;
};

dictionary StorageEventInit : EventInit {
    DOMString? key;
    DOMString? oldValue;
    DOMString? newValue;
    DOMString url;
    Storage? storageArea;
};

Допускает ли localStorage прототипирование?

Storage.prototype.removeItems = function() {
    for (item in arguments) {
        this.removeItem(arguments[item])
    }
}

localStorage.setItem('first', 'some value')
localStorage.setItem('second', 'some value')

localStorage.removeItems('first', 'second')

console.log(localStorage.length) // 0

Как проверить наличие данных в localStorage?

// способ 1
localStorage.setItem('name', 'Harry')

function isExist(name) {
    return (!!localStorage[name])
}

isExist('name') // true

// способ 2
function isItemExist(name) {
    return (name in localStorage)
}

isItemExist('name') // true

В браузере localStorage можно найти здесь:

Довольно слов, пора переходить к делу.

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

Запоминаем время воспроизведения видео

window.onload = () => {
    // находим элемент <video>
    let video = document.querySelector('video')
    
    // если localStorage содержит значение currentTime (текущее время), присваиваем это значение video.currentTime
    if(localStorage.currentTime) {
        video.currentTime = localStorage.currentTime
    }
    
    // при каждом изменении video.currentTime, записываем его значение в localStorage.currentTime
    video.addEventListener('timeupdate', () => localStorage.currentTime = video.currentTime)
}

Результат выглядит так:

Запускаем видео и останавливаем воспроизведение на третьей секунде, например. Время, на котором мы остановились, хранится в localStorage. Чтобы в этом убедиться, перезагружаем или закрываем/открываем страницу. Видим, что текущее время воспроизведения видео остается прежним.

Codepen

Github

Работаем с формой для входа

Разметка выглядит так:

<form>
    Login: <input type="text">
    Password: <input type="text">
    <input type="submit">
</form>

У нас имеется форма и три «инпута». Для пароля мы используем <input type = «text»>, поскольку если использовать правильный тип (password), Chrome будет пытаться сохранять введенные данные, что помешает нам реализовать собственный функционал.

JavaScript:

// находим форму и инпуты для ввода логина и пароля
let form = document.querySelector('form')
let login = document.querySelector('input')
let password = document.querySelector('input + input')

// если localStorage не пустой
// получаем из него необходимые данные
// и присваиваем их инпутам
if (localStorage.length != 0) {
    login.value = localStorage.login
    password.value = localStorage.password
}

// вешаем на форму обработчик события "submit"
form.addEventListener('submit', () => {
    // записываем введенные пользователем данные в localStorage
    localStorage.login = login.value
    localStorage.password = password.value
    
    // если пользователем введены hello и world в качестве логина и пароля, соответственно
    // используем древний метод для вывода сообщения "welcome" на страницу
    if (login.value == 'hello' && password.value == 'world') {
        document.write('welcome')
    }
})

Мы не «валидируем» форму, потому что наша цель — изучить возможности localStorage. Отсутствие валидации, в частности, позволяет записывать пустые строки в качестве логина и пароля.

Результат выглядит так:

Вводим волшебные слова.

Данные записываются в localStorage, а на страницу выводится приветствие.

Codepen

Github

Пишем логику списка задач

Разметка выглядит так:

<input type="text"><button class="add">add task</button><button class="clear">clear storage</button>

<ul></ul>

У нас имеется «инпут» для ввода задачи, кнопка для добавления задачи в список, кнопка для очистки списка и хранилища и контейнер для списка.

JavaScript:

// находим инпут и фокусируемся на нем
let input = document.querySelector('input')
input.focus()
// находим кнопку для добавления задачи в список
let addButton = document.querySelector('.add')
// находим контейнер для списка
let list = document.querySelector('ul')

// если в localStorage имеются данные
if (localStorage.length != 0) {
    // цикл по количеству пар ключ/значение
    for (let i = 0; i < localStorage.length; i++) {
        let key = localStorage.key(i)
        // получаем шаблон - элемент списка
        let template = `${localStorage.getItem(key)}`
        // помещаем задачу в список
        list.insertAdjacentHTML('afterbegin', template)
    }
    
    // находим все кнопки с классом "close" - галочки для выполненных задач
    document.querySelectorAll('.close').forEach(b => {
        // для каждой кнопки
        b.addEventListener('click', e => {
            // получаем родительский элемент "li"
            let item = e.target.parentElement
            // удаляем задачу из списка
            list.removeChild(item)
            // удаляем данные из localStorage
            localStorage.removeItem(`${item.dataset.id}`)
        })
    })
}

// возможность добавления задачи в список при нажатии клавиши "Enter"
window.addEventListener('keydown', e => {
    if (e.keyCode == 13) addButton.click()
})

// вешаем на кнопку для добавления задачи в список обработчик события "клик"
addButton.addEventListener('click', () => {
    // получаем строку - задачу
    let text = input.value
    // формируем шаблон, запись и идентификация значений по ключам осуществляется через атрибут "data-id"
    let template = `<li data-id="${++localStorage.length}"><button class="close">V</button><time>${new Date().toLocaleDateString()}</time> <p>${text}</p></li>`
    // добавляем шаблон - задачу в список
    list.insertAdjacentHTML('afterbegin', template)
    // записываем данные в localStorage
    localStorage.setItem(`${++localStorage.length}`, template)
    // сбрасываем значение инпута
    input.value = ''

    // вешаем на кнопку для удаления задачи из списка обработчик события "клик"
    document.querySelector('.close').addEventListener('click', e => {
        // получаем элемент списка - родительский элемент кнопки
        let item = e.target.parentElement
        // удаляем задачу из списка
        list.removeChild(item)
        // удаляем данные из localStorage
        localStorage.removeItem(`${item.dataset.id}`)
    })
})

// вешаем на кнопку для очистки обработчик события "клик"
document.querySelector('.clear').onclick = () => {
    // очищаем хранилище
    localStorage.clear()
    // удаляем задачи из списка
    document.querySelectorAll('li').forEach(item => list.removeChild(item))
    // фокусируемся на инпуте
    input.focus()
}

Результат выглядит так:

Задачи, добавляемые в список, сохраняются в localStorage в виде готовой разметки. При перезагрузке страницы список формируется из данных хранилища (имеет место перемешивание, о котором упоминалось выше).

Удаление задачи из списка посредством нажатия зеленой галочки приводит к удалению соответствующей пары ключ/значение из хранилища.

Codepen

Github

Схема корзины для товаров

Мы не преследуем цель создать полнофункциональную корзину, поэтому код будет написан «в старом стиле».

Разметка одного товара выглядит так:

<div class="item">
    <h3 class="title">Item1</h3>
    <img src="http://placeimg.com/150/200/tech" alt="#">
    <p>Price: <span class="price">1000</span></p>
    <button class="add" data-id="1">Buy</button>
</div>

У нас имеется контейнер для товара, наименование, изображение и цена товара, а также кнопка для добавления товара в корзину.

Также у нас имеется контейнер для кнопок отображения содержимого корзины и очистки корзины и хранилища и контейнер для корзины.

<div class="buttons">
    <button id="open">Cart</button>
    <button id="clear">Clear</button>
</div>
<div id="content"></div>

JavaScript:

// находим товары и корзину
let itemBox = document.querySelectorAll('.item'),
    cart = document.getElementById('content');

// получаем данные из localStorage
function getCartData() {
    return JSON.parse(localStorage.getItem('cart'));
}

// записываем данные в хранилище
function setCartData(o) {
    localStorage.setItem('cart', JSON.stringify(o));
}

// добавление товара в корзину
function addToCart() {
    // блокируем кнопку на время работы с корзиной
    this.disabled = true;
    // получаем данные из корзины или создаем новый объект, если данные отсутствуют
    let cartData = getCartData() || {},
        // родительский элемент кнопки "Buy"
        parentBox = this.parentNode,
        // id товара
        itemId = this.getAttribute('data-id'),
        // название товара
        itemTitle = parentBox.querySelector('.title').innerHTML,
        // стоимость товара
        itemPrice = parentBox.querySelector('.price').innerHTML;
    // +1 к товару
    if (cartData.hasOwnProperty(itemId)) {
        cartData[itemId][2] += 1;
    } else {
        // + товар
        cartData[itemId] = [itemTitle, itemPrice, 1];
    }
    // обновляем данные в localStorage
    if (!setCartData(cartData)) {
        // снимаем блокировку кнопки
        this.disabled = false;
    }
}

// устанавливаем обработчик события "клик" на каждую кнопку "Buy"
for (let i = 0; i < itemBox.length; i++) {
    itemBox[i].querySelector('.add').addEventListener('click', addToCart)
}

// содержимое корзины
function openCart() {
    // получаем данные из хранилища
    let cartData = getCartData(),
        totalItems = '',
        totalGoods = 0,
        totalPrice = 0;
    // формируем данные для вывода
    if (cartData !== null) {
        totalItems = '<table><tr><th>Name</th><th>Price</th><th>Amount</th></tr>';
        for (let items in cartData) {
            totalItems += '<tr>';
            for (let i = 0; i < cartData[items].length; i++) {
                totalItems += '<td>' + cartData[items][i] + '</td>';
            }
            totalItems += '</tr>';
            totalGoods += cartData[items][2];
            totalPrice += cartData[items][1] * cartData[items][2];
        }
        totalItems += '</table>';
        cart.innerHTML = totalItems;
        cart.append(document.createElement('p').innerHTML = 'Goods: ' + totalGoods + '. Price: ' + totalPrice);
    } else {
        // если в корзине пусто
        cart.innerHTML = 'empty';
    }
}
// открываем корзину
document.getElementById('open').addEventListener('click', openCart);

// очищаем корзину
document.getElementById('clear').addEventListener('click', () => {
    localStorage.removeItem('cart');
    cart.innerHTML = 'сleared';
});

Результат выглядит так:

Выбранные товары записываются в хранилище в виде одной пары ключ/значение.

При нажатии кнопки «Cart» данные из localStorage выводятся в таблицу, подсчитывается общее количество товаров и их стоимость.

Codepen

Github

Благодарю за внимание.

Специально для сайта ITWORLD.UZ. Новость взята с сайта Хабр