Cookies, Storages, IndexedDB, History APi, PaymentRequest Api
Кристина Ильенко
В данной лекции обсуждается браузерное API
document.cookie = 'subject=Song';
document.cookie = 'subject=Beautiful-song';
document.cookie = 'volume=5';
document.cookie = 'subject=' + encodeURIComponent('Песня');
// %D0%9F%D0%B5%D1%81%D0%BD%D1%8F
uniq key = name + path + domain
document.cookie = 'subject=Song; path=/users; domain=yandex.ru';
path=/
domain=music.yandex.ru;
path=/users/playlists;
path=/usersplaylists; domain=yandex.ru;
domain=facebook.com;
subject=Song; expires=Tue, 19 Apr 2019 00:00:00 GMT
Чтобы удалить, устанавливаем дату устаревания в прошлом
subject=Song; expires=Tue, 19 Apr 1970 00:00:00 GMT
document.cookie = 'subject=Song; path=/';
console.log(document.cookie);
// subject=Song
document.cookie = 'subject=Song; path=/';
document.cookie = 'subject=Song; path=/users';
console.log(document.cookie);
Раньше будет выведена cookie с заданным path=/users или с path=/?
Если подходят несколько – доступны все в порядке от наиболее специфичной к наименее
document.cookie = 'subject=Song; path=/';
document.cookie = 'subject=Song; path=/users';
console.log(document.cookie);
subject=Song;
subject=Song;
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)
В cookie храните id, по которому можно на сервере получить полные данные
Кодируйте 01100101
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('repeat', 1);
// QUOTA_EXCEEDED_ERROR
Хранит строки, а не объекты
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'
}
try {
localStorage.setItem('options', '8');
} catch (error) {
console.error(error);
// SecurityError: The operation is insecure
}
Хранение настроек
Хранение промежуточных данных
Строго ограничено источником (origin)
Синхронный интерфейс
Асинхронный интерфейс к SQLite базе
const db = openDatabase('my-app', '1.0', null, 1024 * 1024);
db.transaction(tr => {
tr.executeSql(`
create table if not exists notes(
name TEXT
)
`);
tr.executeSql(`
insert into notes(name)
values("films")
`);
}, console.error);
Строго ограничено источником (origin)
Нет ограничений на размер*
Асинхронное
Не реляционное, а object storage
Не SQL, а API
Хранилище объектов всегда сортирует значения по ключам внутри
Поэтому запросы, возвращающие много значений, всегда возвращают их в отсортированном порядке
// Указываем название и версию
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: 'films',
name: 'films'
});
request.onerror = err => console.error(err);
transaction.abort();
Транзакция остаётся “живой“, если есть активные запросы к концу event loop цикла
Может быть несколько паралелльных readonly транзакций, но только одна readwrite
readwrite транзакция “блокирует” хранилище для записи
// Открываем транзакцию
// Указываем, к каким Store будет иметь доступ транзакция
const transaction = db.transaction(['notes'], 'readonly');
// В рамках транзакции получаем ссылку на объект Store
const store = transaction.objectStore('notes')
// Выполняем запрос в хранилище
// Получаем данные, используя значения ключа (id)
const request = store.get('films')
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', true]));
// ...
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);
});
Разработчики научились загружать отдельные страницы с помощью JavaScript так, чтобы одновременно:
- менялось содержание
- генерировался новый адрес страницы
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.onpopstate = event => {
console.info("location: " + document.location + ", state: " +
JSON.stringify(event.state));
};
window.history.pushState({ page: 1 }, "title 1", "?page=1");
window.history.pushState({ page: 2 }, "title 2", "?page=2");
window.history.pushState({ page: 3 }, "title 3", "?page=3");
window.history.pushState({ page: 4 }, "title 4", "?page=4");
window.history.back();
window.history.replaceState(state, title [, url]) – метод, обновляющий текущее состояние истории браузера
Cобытие popstate
Не вызывает событие
window.history.pushState()
window.history.popState()
Вызывает событие
window.history.back()
window.history.forward()
Совершение действий в браузере
(нажатие стрелок для движения по истории)
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();
Добавляет возможность оплаты «одной кнопкой» на многих сайтах
Cохраняет данные карты у себя
Транзакции PayPal обходятся ритейлерам до 1,9% от выручки
Страницы оформления заказа у ритейлеров практически не стандартизированы
Адрес доставки и выставления счетов должны быть введены отдельно в разных форматах в конце процесса
PR API позволяет хранить данные в браузере
Транзакции бесплатны для ритейлеров
Страницы оформления заказа стандартизованы
Помочь пользователям оплатить так, как им хочется
Сделать это быстро и эффективно
Cоздать простой и удобный способ оформления заказа
Не вводить вручную платёжные данные при каждой покупке, а хранить реквизиты банковских карточек в браузере и вписывать их в форму автоматически
const paymentRequest = new PaymentRequest(
supportedPaymentMethods, // способы оплаты
orderDetails, // детали заказа
paymentOptions // [данные об оплате]
);
Конструктор устанавливает начальное состояние "Created"
Метод show() меняет на "Interactive"
Метод abort() или любая ошибка приводят к состоянию "Closed"
Пользователь принимает или отклоняет платеж – также "Closed"
- Стандартизированные способы оплаты
- Способы оплаты на основе URL
Имеют реестр способов оплаты
- basic-card
- basic-credit-transfer
- tokenized-card
- sepamail
Cвязаны с определенным платежным приложением
Не имеют реестра способов оплаты
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" } ];
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' } } };
PR API не выполняет арифметическую проверку
Разработчик приложения отвечает за то, что значение в total будет соответствовать значениям в displayItems
const paymentRequest = new PaymentRequest( paymentMethods, orderDetails, paymentOptions );
// Пользователю будет предложено указать // адрес электронной почты, имя, номер телефона, адрес доставки // и тип доставки, например: const paymentOptions = { requestPayerEmail: true, requestPayerPhone: true, requestShipping: true };
const paymentRequest = new PaymentRequest(
paymentMethods, // способы оплаты
orderDetails, // детали заказа
paymentOptions // [данные об оплате]
);
paymentRequest
.show()
.then(paymentResponse => {
const paymentData = { ... };
return fetch('/paymentGatewayAddress', paymentData)
.then(response => paymentResponse.complete(...)})
.then(() => successPanel.innerHTML = 'Order complete!')
.catch(() => paymentResponse.complete('fail'));
})
.catch(() => {
failPanel.innerHTML = 'Order failed';
});
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: '0000000000000000',
// 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(...);
});
paymentRequest
.show()
.then(paymentResponse => {
const paymentData = { ... };
return fetch('/paymentGatewayAddress', paymentData)
.then(response => {
if (response.status === 200) {
return paymentResponse.complete('success');
} else {
return paymentResponse.complete('fail');
}
})
.then(() => document.getElementById('status')
.innerHTML = 'Order complete!')
.catch(() => paymentResponse.complete('fail'));
})
.catch(() => document.getElementById('status')
.innerHTML = 'Order failed!';);