Cookies, Storages, IndexedDB, History Api, PaymentRequest Api
Тимур Зубаиров
document.cookie = 'subject=Song';
document.cookie = 'subject=Beautiful-song';
document.cookie = 'volume=5';
document.cookie = 'subject=' + encodeURIComponent('Песня');
console.log(document.cookie)
"volume=5; subject=%D0%9F%D0%B5%D1%81%D0%BD%D1%8F;"
document.cookie = 'subject=Song; path=/songs'
document.cookie = 'subject=Song; domain=music.yandex.ru'
document.cookie = 'subject=Song; max-age=3600'
// Количество секунд с текущего момента
document.cookie = 'subject=Song; expires=Tue, 19 Apr 2021 00:00:00 GMT'
// Дата окончания жизни
uniq key = name + path + domain
document.cookie = 'subject=Song1; path=/';
document.cookie = 'subject=Song2; path=/users';
document.cookie = 'subject=Song3; path=/users; domain=maps.yandex.ru';
console.log(document.cookie);
"subject=Song1; subject=Song2; subject=Song3"
// Последовательно от менее специфичному к более специфичному
document.cookie = 'subject=Song; path=/';
console.log(document.cookie);
"subject=Song"
Чтобы удалить, устанавливаем дату устаревания в прошлом
subject=Song; expires=Tue, 19 Apr 1970 00:00:00 GMT
subject=Song; max-age=0
Cookies.set('subject', 'Song', { expires: 7, path: '' });
Cookies.get('subject');
Cookies.remove('subject');
GET / HTTP/1.1
Host: music.yandex.ru
Cookie: subject=Song; playlist=1
const express = require('express')
const app = express();
const cookieParser = require('cookie-parser');
app.use(cookieParser());
app.use((req, res) => {
console.log(req.cookies);
// { subject: 'Song' }
});
var express = require('express')
var app = express();
app.use((req, res) => {
res.cookie('subject', 'Song', { path: '/users' });
});
HTTP/1.1 200 OK
Set-Cookie: subject=Song; path=/users
res.cookie('subject', 'Song', {
path: '/users',
httpOnly: true
});
HTTP/1.1 200 OK
Set-Cookie: subject=Song; path=/users; HttpOnly
Не доступны в js-скриптах на клиенте
res.cookie('subject', 'Song', {
path: '/users',
secure: true
});
HTTP/1.1 200 OK
Set-Cookie: subject=Song; path=/users; secure
Не доступны по HTTP
Устаревание
Доступ с сервера
4Kb
Передаются с каждым запросом в заголовке, который не сжимается
Для статики используйте
cookieless
домены (CDN)*
Актуально для HTTP 1
У HTTP 2 / HTTP 3 есть сжатие заголовков
В cookie храните id, по которому можно на сервере получить полные данные
Using the Same-Site Cookie Attribute to Prevent CSRF Attacks
Не передаёт данные на сервер
Ограничение в 10Mb
localStorage.setItem('volume', 8);
localStorage.getItem('volume'); // "8"
localStorage.removeItem('volume')
localStorage.clear();
Хранит строки, а не объекты
localStorage.setItem('options', { volume: 8 });
localStorage.getItem('options'); // "[object Object]"
localStorage.setItem('options', JSON.stringify({ volume: 8 }));
window.addEventListener('storage', event => {
console.log(event);
});
{
key: 'volume',
oldValue: '8',
newValue: '6'
}
Хранение настроек
Хранение промежуточных данных
Строго ограничено источником (origin)
Синхронный интерфейс
Строго ограничено источником (origin)
Нет ограничений на размер*
Асинхронное
Не реляционное, а object storage
// Указываем название и версию
const requestDb = indexedDB.open('my-app', 1);
// Метод open возвращает объект IDBOpenDBRequest
// с тремя обработчиками:
// onerror, onsuccess, onupgradeneeded
requestDb.onerror = event => {
console.log(event.target.errorCode);
};
requestDb.onsuccess = event => {
// Экземпляр открытой базы напрямую не отдает
// Получаем доступ к базе
const db = event.target.result;
};
// Будет вызван в первый раз, и когда сменилась версия
requestDb.onupgradeneeded = event => {
const db = event.target.result;
const oldVersion = event.oldVersion;
// Инструкции к миграции на вторую версию
if (oldVersion < 2) {
// Создаём хранилище для заметок
// IndexedDB оперирует не таблицами,
// а хранилищами объектов: ObjectStore
db.createObjectStore('notes', {
keyPath: 'id', // Имя ключевого поля
autoIncrement: true
});
}
}
Можем добавлять/удалять/обновлять данные вне обработчика onupgradeneeded
Можем создавать/измененять хранилище объектов только при обновлении версии БД внутри обработчика onupgradeneeded
Это обеспечивает целостность базы данных (в случае сбоя операции транзакция откатывается)
// Открываем транзакцию
// Указываем, к каким Store будет иметь доступ транзакция
const transaction = db.transaction('notes', 'readwrite');
// В рамках транзакции получаем ссылку на объект Store
const store = transaction.objectStore('notes');
// Выполняем запрос в хранилище
// Добавляем заметку
const request = store.add({
id: 1,
name: 'films'
});
request.onsuccess = (event) => console.error(event.target.result);
request.onerror = err => console.error(err);
Может быть несколько паралелльных readonly транзакций, но только одна readwrite
readwrite транзакция “блокирует” хранилище для записи
// Открываем транзакцию
// Указываем, к каким Store будет иметь доступ транзакция
const transaction = db.transaction(['notes'], 'readonly');
// В рамках транзакции получаем ссылку на объект Store
const store = transaction.objectStore('notes')
// Выполняем запрос в хранилище
// Получаем данные, используя значения ключа (id)
const request = store.get(1)
request.onsuccess = note => console.log(note)
А если хотим пройти через все записи в ObjectStore?
Курсор – особый объект, который пересекает ObjectStore с заданным запросом и возвращает по одному ключу/значению за раз, таким образом экономя память
const requestCursor = db
.transaction(['notes'], 'readonly')
.objectStore('notes')
.openCursor();
requestCursor.onsuccess = event => {
const cursor = event.target.result;
if (cursor) {
console.log(cursor.key, cursor.value);
cursor.continue();
} else {
console.log("No more notes");
}
};
Основное отличие курсора в том, что request.onsuccess срабатывает несколько раз: по одному разу для каждого результата
Например, нужно найти Заметки с именем Films?
Индекс – это “надстройка“ к хранилищу, отслеживающая заданное поле объекта.
Для каждого значения этого поля хранится список ключей для объектов, имеющих это значение.
const db = event.target.result;
const store = db.createObjectStore('notes', {
keyPath: 'id',
autoIncrement: true
});
store.createIndex(
'nameIdx', // Название
'name', // Поле или массив ['name', 'keywords'],
// по которому будем искать
{ unique: false } // Параметры
);
const transaction = db.transaction(['notes'], 'readonly');
const store = transaction.objectStore('notes')
const requestCursor = store
.index('nameIdx')
.openCursor(IDBKeyRange.only('films'));
// ...
const db = new Dexie('MyDatabase');
db
.version(1)
.stores({
notes: 'name'
});
db
.open()
.catch(error => console.error(error));
db
.notes
.where('name')
.equals(['films'])
.each(note => {
console.log(note.name);
});
History API опирается на один DOM интерфейс — объект History. Он доступен через window.history
window.history.length
window.history.state
window.history.go(n)
window.history.back()
window.history.forward()
window.history.pushState(data, title [, url])
window.history.replaceState(data, title [, url])
window.history.length – cвойство length хранит количество записей в текущей сессии истории
window.history.length
// 10
window.history.state – cвойство state хранит текущий объект истории
window.history.state
// { history.id: 0 }
window.history.go(n) – метод, позволяющий гулять по истории. В качестве аргумента передается смещение относительно текущей позиции
window.history.back() – метод, позволяющий гулять по истории. Идентичен вызову window.history.go(-1)
window.history.back()
window.history.forward() – метод, позволяющий гулять по истории. Идентичен вызову window.history.go(1)
window.history.pushState(state, title [, url]) – метод, добавляющий новое состояние в историю браузера
state – любой валидный тип в JavaScript, который можно сериализовать
title – все современные браузеры игнорируют этот параметр
url – относительный/абсолютный URL новой записи в истории браузера
window.history.replaceState(state, title [, url]) – метод, обновляющий текущее состояние истории браузера
Cобытие popstate
Не вызывает событие
window.history.pushState()
window.history.replaceState()
Вызывает событие
window.history.back()
window.history.forward()
window.history.go(-2)
Совершение действий в браузере
(нажатие стрелок для движения по истории)
window.onpopstate = event => {
console.info("onpopstate-event", event);
};
window.history.pushState({ page: 1 }, "title 1", "?page=1");
window.history.pushState({ page: 2 }, "title 2", "?page=2");
window.history.back();
Payment Request API позволяет хранить данные в браузере
Страницы оформления заказа стандартизованы
const paymentRequest = new PaymentRequest(
supportedPaymentMethods, // способы оплаты
orderDetails, // детали заказа
paymentOptions // [данные об оплате]
);
const paymentRequest = new PaymentRequest( paymentMethods, orderDetails, paymentOptions );
const paymentMethods = [ { supportedMethods: 'basic-card', data: { supportedNetworks: [ // "бренды" поддерживаемых карт 'visa', 'mastercard', 'unionpay' ], supportedTypes: [ // типы поддерживаемых карт 'debit', 'credit' ] } }, { supportedMethods: "https://apple.com/apple-pay" } ];
- Стандартизированные способы оплаты
- Способы оплаты на основе URL
Имеют реестр способов оплаты
- basic-card
Cвязаны с определенным платежным приложением
Не имеют реестра способов оплаты
const paymentRequest = new PaymentRequest( paymentMethods, orderDetails, paymentOptions );
const orderDetails = { displayItems: [{ label: '1 x Футболка ДММ', amount: { currency: 'RUB', value: '650.00' } }], total: { label: 'ДММ мерч', amount: { currency: 'RUB', value: '650.00' } } };
const paymentRequest = new PaymentRequest( paymentMethods, orderDetails, paymentOptions );
// Пользователю будет предложено указать // адрес электронной почты, имя, номер телефона, адрес доставки // и тип доставки, например: const paymentOptions = { requestPayerEmail: true, requestPayerPhone: true, requestShipping: true };
paymentRequest.show()
paymentRequest.show()
paymentRequest.show()
paymentRequest
.show().then(paymentResponse => console.info(paymentResponse));
// {
// methodName: 'basic-card',
// details: {
// billingAddress: {
// city: 'Москва',
// country: 'RU',
// organization: 'Яндекс',
// phone: '+79991233212',
// ...
// },
// cardNumber: '1234123412341234',
// cardSecurityCode: '123',
// cardholderName: 'ILYENKO KRISTINA',
// expiryMonth: '06',
// expiryYear: '2020'
// }
// ...
// }
paymentRequest
.show()
.then(paymentResponse => {
const paymentInfo = {
details: paymentResponse.details,
methodName: paymentResponse.methodName
};
const paymentData = {
body: JSON.stringify(paymentInfo),
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
method: 'POST'
};
return fetch('/paymentGatewayAddress', paymentData)
.then(...)
.catch(...);
});