Доброго времени суток, друзья!
В данной статье мы рассмотрим парочку примеров использования Web Storage API или объекта «Storage».
Что конкретно мы будем делать?
- Научимся запоминать время воспроизведения видео.
- Поработаем с формой входа на страницу.
- Напишем логику списка задач (todo list).
- Реализуем чат
- Схематично набросаем корзину для товаров.
Итак, поехали.
Краткий обзор
Объект «Storage» используется для хранения данных на стороне клиента и в этом смысле выступает альтернативой cookies. Преимущество Storage состоит в размере хранилища (от 5 Мб, зависит от браузера, при превышении лимита выбрасывается ошибка «QUOTA_EXCEEDED_ERR») и отсутствии необходимости обращаться к серверу. Существенный недостаток — безопасность: стоит вредоносному скрипту получить доступ к странице, и пиши пропало. Поэтому крайне не рекомендуется хранить в Storage конфиденциальную информацию.
Справедливости ради стоит отметить, что на сегодняшний день существуют более продвинутые решения для хранения данных на стороне клиента — это IndexedDB, 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, а на страницу выводится приветствие.
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
Реализация чата
Разметка выглядит так:
<input type="text">
<button class="send">send message</button>
<button class="save">save chat</button>
<button class="clear">clear chat</button>
<div></div>
У нас имеется инпут для ввода сообщения, три кнопки: для отправки сообщения, для сохранения переписки и для очистки чата и хранилища, а также контейнер для сообщений.
JavaScript:
// находим инпут и фокусируемся на нем
let input = document.querySelector('input')
input.focus()
// находим кнопки
let sendButton = document.querySelector('.send')
let saveButton = document.querySelector('.save')
let clearButton = document.querySelector('.clear')
// находим контейнер
let box = document.querySelector('div')
// если в хранилище имеется ключ "messages"
if (localStorage.messages) {
// формируем переписку
localStorage.messages
.split('</p>,')
.map(p => box.insertAdjacentHTML('beforeend', p))
}
// обрабатываем отправку сообщений
sendButton.addEventListener('click', () => {
// получаем текст сообщения
let text = document.querySelector('input').value
// формируем шаблон
let template = `<p><time>${new Date().toLocaleTimeString()}</time> ${text}</p>`
// добавляем шаблон в контейнер
box.insertAdjacentHTML('afterbegin', template)
// сбрасываем значение инпута
input.value = ''
// записываем сообщение в хранилище
localStorage.message = template
})
// добавляем задачу в список при нажатии "Enter"
window.addEventListener('keydown', e => {
if (e.keyCode == 13) sendButton.click()
})
// обрабатываем событие "storage"
window.addEventListener('storage', event => {
// если ключом события является "messages"
// игнорируем его
if (event.key == 'messages') return
// если новое значение события равняется null
// нажимаем кнопку для очистки хранилища
// иначе добавляем сообщение в контейнер
event.newValue == null ? clearButton.click() : box.insertAdjacentHTML('afterbegin', event.newValue)
})
// сохраняем переписку
saveButton.addEventListener('click', () => {
// массив сообщений
let messages = []
// заполняем массив
document.querySelectorAll('p').forEach(p => messages.push(p.outerHTML))
// записываем данные в хранилище
localStorage.messages = messages
})
// очищаем хранилище и контейнер
clearButton.addEventListener('click', () => {
localStorage.clear()
document.querySelectorAll('p').forEach(p => box.removeChild(p))
input.focus()
})
Результат выглядит так:
Отправляемое сообщение записывается в localStorage.message. Событие «storage» позволяет организовать обмен сообщениями между вкладками браузера.
При сохранении чата все сообщения записываются в localStorage.messages. При перезагрузке страницы из записанных сообщений формируется переписка.
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
Благодарю за внимание.
Kolobok86
Это точно. Казалось бы, прописная истина, но всё равно встречаются решения, хранящие JWT токен в localStorage. Делать так не надо.
Bonio
А где лучше хранить токены?
mrFox
HttpOnly Secure cookie
Bonio
А если токенов несколько и они используются javascript приложением?
mrFox
Значит Вы неправильно используете JWT