Веб-браузеры снабжены хранилищами, в которых веб-приложения могут хранить, например, пользовательские предпочтения, или даже свое полное состояние, чтобы восстанавливаться в точности их конца последнего посещения.
Хранилища разделяются по источнику и потому страницы одного сайта не могут читать данные из хранилища другого, однако страницы одного и того же сайта могут.
Виды хранилищ на стороне клиента:
Важно знать простой факт. Ни одно из выше перечисленных хранилиц не предполагает шифрования. Вся информация сохраняется на стороне клиента в незашифрованном виде и потому существует риск нарушения конфеденциальности со стороны юзеров совместно использующих устройство или же вредоносного ПО. И потому ни одна из форм хранилищ на стороне клиента никогда не должна использоваться для хранения паролей, банковских счетов или другой конфиденциальной информации.
Объект Window имеет свойства localStorage и sessionStorage, которые ссылаются на объект Storage, который, в сою очередь, имеет следующие отличительные черты:
Стандартный метод извлечения данных из Storage:
<script>
var user_id='Chernyshev Egor'
if (localStorage.getItem(user_id)){
user_data=JSON.parse(localStorage.getItem(user_id))
}
</script>
Получим содержимое localStorage нашего клиента:
<script>
function loc_st_print(){
document.querySelector('#locst_ex1').innerHTML='<ol>'
var lst=localStorage
lst_list=[]
for (var i = 0; i < lst.length; i++){
lst_list.push(`<li>${lst.key(i)} : ${lst.getItem(lst.key(i))}</li>`)
}
for (var i=0; i < lst.length; i++){
document.querySelector('#locst_ex1').innerHTML=document.querySelector('#locst_ex1').innerHTML+lst_list[i]
}
document.querySelector('#locst_ex1').innerHTML=document.querySelector('#locst_ex1').innerHTML+'</ol>'
}
</script>
<button onclick="loc_st_print()">Print <span class="code">localStorage</span></button>
<div id="locst_ex1"></div>
Стандартный метод записи данных в Stoage:
<script>
var user_id='Chernyshev Egor'
var user_data={
occupation: 'web-analyst',
age: 27,
education: 'bachelor degree'
}
localStorage.setItem(user_id, JSON.stringify(user_data))
</script>
<script>
document.addEventListener('DOMContentLoaded', function(){
if (localStorage.name){
document.querySelector('#loc_st_name').innerHTML=localStorage.name
}
else {
document.querySelector('#loc_st_name').innerHTML = 'Нет имени'
}
})
function loc_st_name_save(){
let lst_name=document.getElementById('name_str_to_loc_st').value
if (lst_name){
localStorage.name=lst_name
document.querySelector('#loc_st_name').innerHTML=localStorage.name
}
else {
document.querySelector('#loc_st_name').innerHTML = 'Нет имени'
}
}
</script>
<div>Введите имя:<input type="text" id="name_str_to_loc_st"></div>
<button onclick="loc_st_name_save()">Спросите меня мое имя!</button>
<div><b>Ваше имя в <span class="code">localStorage</span>: </b><span id="loc_st_name"></span></div>
Т.к. объект Storage способен хранить только строковые значения, то нам необходим метод их кодирования/декодирования
<script>
//числа
localStorage.х = 10 //трансмутируемый в строку тип сохранится как строка
//даты
localStorage.lastRead = (new Date()).toUTCString(); //преобразуем в строку
let lastRead = new Date(Date.parse(localStorage.lastRead)); //преобразуем в число
//структуры данных (объекты)
//для этого используется json-код/декод
localStorage.data = JSON.stringify(data);
let data = JSON.parse(localStorage.data);
</script>
Storage | Время жизни | Область видимости хранилища |
---|---|---|
localStorage | Данные localStorage хранятся вечно или до тех пор, пока веб-приложение их не отчистит или юзер сам не их не удалит. | Ограничен источником документа (политика одинакового источника), т.е. протоколом, именем хоста и портом. Все одинаковые источники разделяют один localStorage (на практике, страницы 1 и того же сайта). Они могут читать и перезаписывать данные одного localStorage. Документы разных источников никогда не смогут читать или перезаписывать данные друг друга (даже если через скрипт из одного источника). localStorage также ограничен браузером юзера (нельзя сменить браузер и ожидать того же localStorage) |
sessionStorage | Данные sessionStorage имею время жизни окна верхнего уровня (вкладки браузера, где выполняется скрипт их сохранения). При закрытии окна данные sessionStorage удаляются навсегда. |
Для sessionStorage справедливы те же ограничения области видимости хранилища, что и для localStorage. Но sessionStorage также ограничивается окнами. 2 окна из одного источника будут иметь 2 разных sessionStorage: у скриптов 1 вкладки нет доступа к localStorage другой вкладки, даже когда это одни и те же вкладки и скрипты. |
При любом изменении данных в localStorage браузер инициирует событие "storage" в любом другом объекте Window, где эти изменения видны (но не в окне, которое вносит изменение). Если открыты 2 вкладки и на одной вносится изменение в localStorage, то другая вкладка получит событие "storage".
Способы регистрации обработчкика событий "storage":
Свойства объекта события storage | Описание |
key | Имя или ключ элемента, который был установлен или удален. Если вызывался метод .clear(), то это свойство = null. |
newValue | Новое значение элемента при его наличии. При вызове removeItem() этого свойства не будет. |
oldValue | Старое значение измененного/удаленного элемента. Если добавляется новое значение, то этого свойства не будет в объекте. |
storageArea | localStorage или sessionStorage |
url | URL документа, чей скрипт изменил хранилище. |
Основные кейсы применения Storage:
Теперь к вопросу о том, зачем это нужно на практике. Например, пользователь потребовал, чтобы сайт прекратил анимацию, тогда сайт может сохранить такое предпочтение в localStorage, чтобы соблюсти его при будущих посещениях. Сохраняя его он может вызвать событие, которое передаст другим окнам это предпочтение и позволит изменить сайт в соответствии с ним в реальном времени.
Теперь допустим, что у сайта есть окно с палитрой инструментов. Когда пользователь настраивает инструмент с материнского окна, то в дочернее окно передает событие изменение localStorage, что также позволяет поддерживать общее состояние в текущем времени.
Задача: необходимо сохранить историю просмотров товаров в интернет-мазагине. Сделаем это применимым для данного сайта, сохраняя значение title-тега каждой страницы, которую посещал пользователь.
Важно заметить: для элементов localStorage возможно задать продолжительность жизни.
<script>
var ls_history
$(document).ready(function(){
const expiration_date=60*60*24*21 //21 день
//этот скрипт ведет историю просмотров страниц сайта
//этот скрипт должен располагаться на выделенном сегменте страниц сайта
//todo: расположить его на каждой странице, чтобы записывать историю
var current_page={
title: document.querySelectorAll('title')[0].innerHTML,
ts: new Date() //отметка времени для контроля над сроком жизни
}
var view_history = localStorage.getItem('view_history')
if (!view_history){
view_history=[]
view_history.push(current_page)
ls_history=JSON.parse(JSON.stringify(view_history))
localStorage.setItem('view_history', JSON.stringify([current_page]))
}
else {
view_history=JSON.parse(view_history)
view_history=checkLS_forUniqness(current_page, view_history)
view_history=checkLS_forExpires(view_history, expiration_date)
view_history.push(current_page)
ls_history=JSON.parse(JSON.stringify(view_history))
localStorage.setItem('view_history', JSON.stringify(view_history))
}
function checkLS_forExpires(list, expires){
var new_list=[]
for (let i=0; i<list.length; i++){
d1=new Date(list[i]['ts']).getTime()
if (!d1<(new Date()).getTime()-expires){
new_list.push(list[i])
}
}
return new_list
}
function checkLS_forUniqness(page, list){
var new_list=[]
for (let i=0; i<list.length; i++){
if (page.title!=list[i].title){
new_list.push(list[i])
}
}
return new_list
}
})
</script>
Выведем историю в виде таблицы:
Теоретически возможно воспроизвести полный функционал стороны клиента cookie для localStorage, т.е. установление вермени и срока жизни элемента,
Задача: сохранить данные заполнения форм, чтобы те сохранялись, даже когда пользователь покидает страницу
<script>
const elems = document.querySelectorAll('#form_ex5 input');
//сохраним значения полей в localStorage
function processField(){
localStorage.setItem(window.location.href, 'true');
localStorage.setItem(this.id, this.value);
}
// Очищаем поля по одному
function clearStored(){
elems.forEach(elem => {
if (elem.type === 'text') {
localStorage.removeItem(elem.id);
}
});
}
// Перехватываем нажатие кнопки Submit и очищаем хранилище
document.getElementById('submit_ex5').onsubmit = clearStored;
// При изменении элемента формы записываем его значение в localStorage
elems.forEach(elem => {
if (elem.type === 'text') {
const value = localStorage.getItem(elem.id);
if (value) elem.value = value;
// Событие change
elem.onchange = processField;
}
});
try {
localStorage.setItem('key', 'value');
} catch (domException) {
if (
['QuotaExceededError', 'NS_ERROR_DOM_QUOTA_REACHED'].includes(
domException.name
)
) {
// Превышен размер файла; обрабатываем ошибку
} else {
// Какая-то другая ошибка; обрабатываем ее
}
}
</script>
Cookie-набор - это небольшой объем данных, который сохранен браузером и ассоциирован с определенным документом. Cookie проектировались для программирования на стороне сервера и на самом низком уровне реализованы как расширение протокола HTTP. Данные cookie передаются автоматически между сервером и браузером, так что скрипты стороны сервера могут читать и записывать cookie, которые хранятся на стороне клиента.
API JS для взаимодействия с cookie радикально устаревший и неудобный. И потому часто приходится писать собственные методы и функции для обработки cookie.
<script>
//функция, возвращающая cookie документа как объект Map.
//предполагает, что cookie закодированы через encodeURIComponent()
function getCookies() {
let cookies = new Map();
let all = document.cookie;
let list=all.split("; ")
for (let cookie of list){
if (!cookie.includes("=")) continue;
let p=cookie.indexOf("=")
let name=cookie.substring(0,p)
value=decodeURIComponent(value)
cookies.set(name, value)
}
return cookies;
}
</script>
Сookie могут использоваться для автозаполнения форм, если они однажды уже были заполнены.
Cookie предназначены для сохранения данных небольшого размера скриптами стороны сервера, а эти данные передаются серверу каждый раз когда запрашивается соответствующий URL.
Первым делом прочтем те cookie, которые уже хранятся на стороне клиента:
<script>
function show_cookie(){
document.querySelector('#cookie_ex_output').innerHTML=document.cookie
}
</script>
Очень удобно читать cookie с помощью API cookieStore. Однако этот способ доступен только через для протокола https.
<script>
cookie_list=[]
cookieStore.getAll().then(cookies=>cookie_list.push(cookies))
console.log(cookie_list)
$(document).ready(function (){
list_of_dicts_to_table(cookie_list[0], '#table_cookie_store_ex')
})
</script>
Атообноление
Самый просто метод записи cookie имеет следующий вид:
<script>
document.cookie=`version=${encodeURIComponent(document.lastModified)}`
</script>
Функция encodeURIComponent(string) кодирует се символы строки, за исключением цифр, латинских букв и - _ . ! ~ * ' ( )
<script>
function set_cookie_input(){
document.cookie=`${encodeURIComponent(document.getElementById('cookie_name_var').value)}=${encodeURIComponent(document.getElementById('cookie-input').value)}`
}
</script>
Значения cookie-наборов не могут содержать точки с запятой, запятые или пробельные символы. По этой причине и применяется URI-кодирование.
Записанный таким вышеописанным образом cookie-набор теряется всякий раз при закрытии браузера, чтобы этого не происходило необходимо задать время жизни cookie-набора. Это делается с помощью свойства max-age=seconds.
Критерий | Cookies | localStorage | sessionStorage |
---|---|---|---|
Максимальный размер | 4 кБ | 5 Мб | 5 Мб |
Блокируемы юзером | Да | Да | Да |
Автоистечение срока | Да | Нет | Да |
Поддерживаемый тип | string | string | string |
Поддержка браузерами | Высокая | Высокая | Высокая |
Доступ со стороны сервера | Да | Нет | Нет |
Передача данных с каждым HTTP-реквестом | Да | Нет | Нет |
Изменяемы юзером | Да | Да | Да |
Поддерживаются на SSL | Да | Нет | Нет |
Доступ есть у... | Клиента и сервера | Клиента | Клиента |
Чистка/удаление | PHP, JS, автоматически | только JS | JS и автоматически |
Срок жизни | Как указано | До удаления | До закрытия вкладки |
Безопасное хранилище | Нет | Нет | Нет |
Риск CSRF-атак | Да | Нет | Нет |
Sessions:
Local:
Cookies:
IndexedDB - объектная база данных (т.е. не реляционная), которая мощнее ,чем localStorage, спроектированный по модели "ключ-значение". IndexedDB также, как и localStorage ограничена одним источником. Каждый источник может иметь неограниченное число таких баз данных.
В API-интерфейсе IndexedDB база данных — это просто коллекция именованных объектных хранилищ. Объектное хранилище запоминает объекты. Объекты сериализируются в объектное хранилище с использованием алгоритма структурированного клонирования, т.е. сохраняемые объекты могут иметь свойства, чьими значениями являются объекты Map, Set или типизированные массивы. Каждый объект обязан иметь ключ, по которому он может сортироваться и извлекаться из хранилища. Ключи должны быть уникальными — два объекта в том же самом хранилище не могут иметь одинаковые ключи — и они должны поддерживать естественное упорядочение, чтобы их можно было сортировать. Допустимыми ключами в JavaScript будут строки, числа и объекты Date. База данных IndexedDB способна автоматически генерировать уникальный ключ для каждого объекта, вставляемого в базу данных.
Помимо извлечения объектов из объектного хранилища по значению их первичного ключа вас может интересовать возможность поиска на основе значений других свойств в объекте. Для этого в объектном хранилище можно определить любое количество индексов. (Наличие возможности индексации и объясняет название IndexedDB.)
IndexedDB обеспечивает гарантии атомарности: запросы и обновления базы Данных группируются внутри транзакции, так что они все вместе успешны или все вместе отказывают и никогда не оставляют базу данных в неопределенном, частично обновленном состоянии. Транзакции в IndexedDB проще, чем во многих API-интерфейсах баз данных; позже мы снова к ним вернемся
Чтобы запросить Или обновить базу данных, сначала вы должны ее открыть (указав имя). Далее вы создаете объект транзакции и используете его для нахождения в базе данных желаемого объектного хранилища, тоже по имени. В заключение вы ищете объект, вызывая метод get() объектного хранилища, либо сохраняете новый объект, вызывая метод put() (или add(), если хотите избежать перезаписывания существующих объектов).
Если вы хотите найти объекты для диапазона ключей, тогда создайте объект IDBRange, который задает верхнюю и нижнюю границы диапазона, и передайте его методу getAll() или openCursor() объектного хранилища.
Если вы желаете сделать запрос с применением вторичного ключа, тогда найДите именованный индекс объектного хранилища и затем вызовите метод get(), getAll() или openCursor() объекта индекса, передавая ему либо одиночный ключ, либо объект IDBRange
Однако концептуальная простота API-интерфейса IndexedDB осложняется тем фактом, что API-интерфейс является асинхронным (поэтому веб-приложения могут его использовать без блокирования главного потока пользовательского интерфейса браузера). API-интерфейс IndexedDB был определен до широкой поддержки объектов Prom ise, так что он основан на событиях, а не на prom ise, т.е. не работает с ключевыми словами async и aw ait.
Создание транзакций и поиск объектных хранилищ и индексов представляют собой синхронные операции. Но открытие базы данных, обновление объектного хранилища и запрашивание хранилища или индекса относятся к асинхронным операциям. Все эти асинхронные методы немедленно возвращают объект запроса. Браузер инициирует события успеха или неудачи в объекте запроса, когда запрос завершается успешно или отказывает, и вы можете определить обработчики с помощью свойств o n su ccess и o n e rro r. Внутри обработчика o n su ccess результат операции доступен как свойство r e s u l t объекта запроса. Еще одно удобное событие, "com plete", отправляется объектам транзакций, когда транзакция успешно завершена
Полезная особенность такого асинхронного API-интерфейса заключается в том, что он упрощает управление транзакциями. API-интерфейс IndexedDB вынуждает вас создавать объект транзакции для получения объектного хранилища, в котором вы можете выполнять запросы и обновления. В синхронном API-интерфейсе вы ожидали бы явной пометки конца транзакции за счет вызова метода commit (). Но в IndexedDB транзакции фиксируются автоматически (если вы их явно не прекращаете), когда все обработчики событий onsuccess были запущены и больше нет незаконченных асинхронных запросов, которые относятся к данной транзакции.
тносятся к данной транзакции. Есть еще одно событие, которое важно для API-интерфейса IndexedDB. Когда вы открываете базу данных в первый раз или инкрементируете номер версии существующей базы данных, IndexedDB инициирует событие "upgradeneeded' в объекте запроса, возвращенном вызовом i n d e x e d D B . open (). Задача обработчика событий "upgrade n e e d e d " — определить или обновить схему для новой базы данных (или новой версии существующей базы данных). В случае баз данных IndexedDB это означает создание объектных хранилищ и определение в них индексов. Фактически API-интерфейс IndexedDB позволяет вам создать объектное хранилище либо индекс только в ответ на событие "upgradeneeded".
Ознакомившись с предложенным высокоуровневым обзором IndexedDB, ®ы должны быть в состоянии понять пример 15.13, в котором IndexedDB применяется для создания и запрашивания базы данных, отображающей почтовые коды США на города. В нем демонстрируются многие (но не все) основные средств* IndexedDB. Код примера 15.13 длинный, но он хорошо прокомментирован.
// Эта служебная функция асинхронно получает объект базы данных ц (при необходимости создавая и инициализируя базу данных) / / и передает ее обратному вызову, function withDB (callback) { let request = indexedDB.open("zipcodes", 1); // Запросить версию 1 // базы данных. request.onerror = console.error; // Сообщать о любых ошибках request.onsuccess =()=>{ // или вызывать это в случае успеха. let db = request.result; // Результатом запроса будет база данных, callback(db); // Вызвать обратный вызов с базой данных. }; // Если версия 1 базы данных пока не существует, тогда будет запущен // этот обработчик событий. Он используется для создания и инициализации // объектных хранилищ и индексов, когда база данных впервые создается, // или для их модификации, когда мы переключаемся с одной версии схемы // базы данных на другую. request.onupgradeneeded = () => { initdb(request.result, callback); }; } // withDB () вызывает эту функцию, если база данных пока еще // не инициализирована. Мы устанавливаем базу данных и наполняем ее данными,. // затем передаем базу данных функции обратного вызова. // // Наша база данных почтовых кодов включает одно объектное хранилище, // которое содержит объекты следующего вида: И И { И zipcode: "02134”, // city: "Allston", // state: "MA", U ) // //Мы используем свойство zipcode в качестве ключа базы данных //и создаем индекс для названия города (свойства city) . function initdb(db, callback) { // Создать объектное хранилище, задав имя хранилища и объект // параметров, который включает "путь к ключу", указывающий // имя свойства поля ключа для этого хранилища, let store = db.createObjectStore ("zipcodes", // Имя хранилища. { keyPath: "zipcode" }); // Индексировать объектное хранилище по названию города и почтовому коду. / / В этом методе строка с путем к ключу передается непосредственно как // обязательный аргумент, а не как часть объекта параметров, store.createlndex("cities", "city"); // Получить данные, которыми мы собираемся инициализировать базу данных. // Файл данных zipcodes. json был сгенерирован из данных, подпадающих // под лицензию Creative Commons и доступных в www.geonames.org: // https://download.geonames.org/export/zip/US.zip. fetch("zipcodes.json") // Сделать HTTP-запрос GET. .then(response => response.j son()) // Разобрать тело как JSON. then(zipcodes «> { // Получить 40 Кбайт записей / / с почтовыми кодами. // Для вставки данных почтовых кодов в базу данных нам необходим // объект транзакции. Чтобы создать объект транзакции, нужно // указать, какие объектные хранилища мы будем использовать // (у нас есть только одно), и сообщить ему, что мы планируем // выполнять запись в базу данных, а не только чтение: let transaction = db.transaction(["zipcodes”], "readwrite"); transaction.onerror = console.error; // Получить наше объектное хранилище из транзакции, let store = transaction, objects tore ("zipcodes"); // Наилучшая характеристика API-интерфейса IndexedDB заключается / / в том, что объектные хранилища * действительно* просты. // Вот как можно добавлять (или обновлять) наши записи: for(let record of zipcodes) { store.put(record); } // После успешного завершения транзакции база данных инициализи- // рована и готова к использованию, так что мы можем вызвать функцию // обратного вызова, которая первоначально была передана withDBO . transaction.oncomplete = () => { callback(db); }; }); } // Для заданного почтового кода функция использует API-интерфейс IndexedDB, // чтобы асинхронно искать название города с таким почтовым кодом, // и передает его указанному обратному вызову или null, если город не найден, function lookupCity(zip, callback) { withDB(db => { // Создать для этого запроса объект транзакции, выполняющей только // чтение. Аргументом является массив объектных хранилищ, // которые необходимо использовать. let transaction = db. transaction(["zipcodes"]); // Получить объектное хранилище из транзакции, let zipcodes = transaction.objectStore("zipcodes"); //Запросить объект,который соответствует указанному ключу почтового кода. // Строки кода выше были синхронными, но следующие строки асинхронные, let request = zipcodes.get(zip); request.onerror = console.error; // Сообщать о любых ошибках request.onsuccess =()=>{ // или вызывать это в случае успеха, let record = request.result; // Это результат запроса, if (record) { // Если совпадение найдено, тогда передать // его обратному вызову, callback(4${record.city}, ${record.state}'); } else { // Иначе сообщить обратному вызову о том, // что ничего не найдено, callback(null); } }; }); } // Для заданного названия города функция использует API-интерфейс IndexedDB, // чтобы асинхронно искать все записи с почтовыми кодами для всех городов // (в любых штатах), которые имеют такое (чувствительное к регистру) название, function lookupZipcodes(city, callback) { withDB(db => { // Как и ранее, мы создаем транзакцию и получаем объектное хранилище, let transaction = db. transact ion (["zipcodes"]); let store = transaction.objectStore("zipcodes"); //На этот раз мы также получаем индекс городов в объектном хранилище, let index = store.index("cities"); // Запросить все совпадающие записи в индексе с указанным названием // города и после получения передать их обратному вызову. / / В случае ожидание большего количества результатов // взамен можно использовать openCursorO . let request = index.getAll(city); request.onerror = console.error; request.onsuccess = () => { callback(request.result); }; }); }