Использования Cache API в Service Workers
Cache API — это система для хранения и извлечения сетевых запросов и соответствующих ответов. Это могут быть обычные запросы и ответы, созданные в ходе работы вашего приложения, или это могут быть запросы созданы исключительно для хранения некоторых данных в кеше.
Cache API был создан для того, чтобы Service Workers могли кэшировать сетевые запросы, чтобы они могли предоставлять соответствующие ответы даже в автономном режиме. Тем не менее, это API также может быть использовано в качестве общего механизма хранения.
Где это доступно?
API в настоящее время доступно в Chrome, Opera и Firefox. И Edge, и Safari помечено «В разработке».
API предоставляется через свойство global caches, так что вы можете проверить наличие API с помощью простого обнаружения функции:
const cacheAvailable = 'caches' in self;
К API можно получить доступ из window, iframe, worker или service worker.
Что можно хранить в кеше
Кэш хранит только пары объектов Request и Response, представляющих HTTP-запросы и ответы соответственно. Однако запросы и ответы могут содержать любые данные, которые можно передавать по HTTP.
Можно создать объект Request, используя URL для сохранения в кеше:
const request = new Request('/images/sample1.jpg');
Конструктор объекта Response принимает разные типы данных, включая Blobs, ArrayBuffers, объекты FormData и строки.
const imageBlob = new Blob([data], {type: 'image/jpeg'}); const imageResponse = new Response(imageBlob); const stringResponse = new Response('Hello world');
Можно установить MIME тип Response, установив соответствующий заголовок.
const options = { headers: { 'Content-Type': 'application/json' }} const jsonResponse = new Response('{}', options);
Работа с объектами Response
Если вы получили Response и хотите получить доступ к его body, вы можете использовать несколько вспомогательных методов. Каждый возвращает Promise, которое разрешается со значением другого типа.
Method | Description |
---|---|
arrayBuffer | Возвращает ArrayBuffer, содержащий body, сериализованное в байты. |
blob | Возвращает Blob Если Response был создан с помощью Blob, то этот новый Blob имеет тот же тип. В противном случае используется Content-Type Response. |
text | Интерпретирует байты body как строку в кодировке UTF-8. |
json | Интерпретирует байты body как строку в кодировке UTF-8, а затем пытается проанализировать ее как JSON. Возвращает результирующий объект или выдает ошибку TypeError, если строка не может быть преобразована в JSON. |
formData | Интерпретирует байты тела как форму HTML, закодированную как «multipart/form-data» или «application/x-www-form-urlencoded». Возвращает объект FormData или выдает ошибку TypeError, если данные не могут быть проанализированы. |
body | Возвращает ReadableStream для данных body. |
Для примера
const response = new Response('Hello world'); response.arrayBuffer().then((buffer) => { console.log(new Uint8Array(buffer)); // Uint8Array(11) [72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100] });
Создание и открытие кеша
Чтобы открыть кеш, используйте метод caches.open(name), передавая имя кеша в качестве единственного параметра. Если именованный кеш не существует, он создается. Этот метод возвращает Promise, который разрешается с помощью объекта Cache.
caches.open('my-cache').then((cache) => { // do something with cache... });
Извлечение из кэша
Чтобы найти элемент в кеше, используется метод match.
cache.match(request).then((response) => console.log(request, response));
Если запрос является строкой, он сначала преобразуется в запрос путем вызова new Request(request). Функция возвращает Promise, который разрешается в Response, если найдена соответствующая запись, или undefined в противном случае.
Чтобы определить, совпадают ли два запроса, используется не только URL-адрес. Два запроса считаются разными, если они имеют разные строки запроса, заголовки Vary и/или методы (GET, POST, PUT и т. д.).
Вы можете игнорировать некоторые или все эти вещи, передавая объект параметров в качестве второго параметра.
const options = { ignoreSearch: true, ignoreMethod: true, ignoreVary: true }; cache.match(request, options).then(...);
Если более одного кэшированного запроса совпадает, возвращается тот, который был создан первым.
Если вы хотите получить все соответствующие ответы, можно использовать cache.matchAll.
const options = { ignoreSearch: true, ignoreMethod: true, ignoreVary: true }; cache.matchAll(request, options).then((responses) => { console.log(`There are ${responses.length} matching responses.`); });
Можно выполнить поиск по всем кешам одновременно, используя caches.match() вместо вызова cache.match() для каждого кеша.
Поиск
Cache API не предоставляет способ поиска запросов или ответов, за исключением сопоставления записей с объектом Response. Однако можно реализовать свой собственный поиск, используя фильтрацию или создав индекс.
Фильтрация
Один из способов реализовать собственный поиск — перебрать все записи и отфильтровать их до тех, которые вам нужны. Допустим, вы хотите найти все элементы, URL-адреса которых заканчиваются на «.png».
async function findImages() { // Получаем список всех кэшей для этого источника const cacheNames = await caches.keys(); const result = []; for (const name of cacheNames) { // Открываем кеш const cache = await caches.open(name); // Получаем список записей. Каждый элемент является объектом Request for (const request of await cache.keys()) { // Если URL запроса совпадает, добавляем ответ к результату if (request.url.endsWith('.png')) { result.push(await cache.match(request)); } } } return result; }
Таким образом, можно использовать любое свойство объектов Request и Response для фильтрации записей. Обратите внимание, что это медленно, если вы ищите большие наборы данных.
Создания index
Другим способом реализации собственного поиска является ведение отдельного индекса записей, которые можно искать, которые хранятся в IndexedDB. Поскольку эта операция была разработана для IndexedDB, она имеет гораздо лучшую производительность при большом количестве записей.
Если вы храните URL-адрес Request вместе со свойствами поиска, можно легко получить правильную запись в кэше после выполнения поиска.
Добавление в кеш
Есть три способа добавить элемент в кеш — put, add и addAll. Все три метода возвращают Promise.
cache.put
Синтаксис cache.put (request, response). request — это либо объект Request, либо строка — если это строка, то вместо нее используется new Request(request). response должен быть объектом Response. Эта пара хранится в кеше.
cache.put('/test.json', new Response('{"foo": "bar"}'));
cache.add
Синтаксис cache.add (request). request обрабатывается так же, как и для put, но Response, который хранится в кэше, является результатом выборки запроса из сети. Если выборка не удалась или если код состояния ответа не находится в диапазоне 200, то ничего не сохраняется и promises отклоняется. Обратите внимание, что запросы между источниками, не находящиеся в режиме CORS, имеют статус 0, и поэтому такие запросы могут храниться только с put.
cache.addAll
Синтаксис cache.addAll (requests), где requests — это массив Request
или строк URL. Это работает аналогично вызову cache.add для каждого отдельного запроса, за исключением того, что Promise отклоняется, если какой-либо отдельный запрос не кэшируется.
В каждом из этих случаев новая запись перезаписывает любую соответствующую существующую запись. При этом используются те же правила сопоставления, которые описаны в разделе о получении.
Удаление
Удаление элемента из кэша:
cache.delete(request);
Где запрос может быть Request или строкой URL. Этот метод также принимает тот же объект параметров, что и cache.match, который позволяет удалить несколько пар «Request/Response» для одного и того же URL-адреса.
cache.delete('/example/file.txt', { ignoreVary: true, ignoreSearch: true });
Удаление кеша
Чтобы удалить кеш, вызовите caches.delete(name). Эта функция возвращает Promise, который принимает значение true, если кэш существовал и был удален, или false в противном случае.
Пример использования кеша для service worker
Простой пример, демонстрирующий использование service worker для предварительного кэширования приложения JavaScript.
Этот пример показывает как можно улучшить взаимодействие пользователя с веб-страницой, которая динамически загружает какое нибудь тяжелое приложение в iFrame.
Проблема
Чтобы проиллюстрировать проблему, мы используем простой пример, состоящий из страницы (index.html) и с кнопкой на ней.
Нажатие на кнопку создает iFrame, который загружает вторую страницу (iframe.html), которая, в свою очередь, загружает приложение JavaScript (script.js); представьте, что этот файл большой (скажем, какое нибудь тяжелое приложение).
Хорошей новостью является то, что мы откладываем загрузку большого файла JavaScript, пока не нажмем кнопку. Плохая новость заключается в том, что после нажатия кнопки браузер должен (медленно) загрузить файл перед запуском приложения JavaScript в iFrame.
Код
Код проблемы особенно прост.
index.html
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Parent</title> </head> <body> <button>Open iframe</button> <script src="app.js"></script> </body> </html>
app.js
var openButtonEl = document.getElementById('open-button'); openButtonEl.addEventListener('click', handleClick); function handleClick() { openButtonEl.setAttribute('disabled', true); var iframeEl = document.createElement('iframe'); iframeEl.src = 'iframe.html'; document.body.appendChild(iframeEl); }
iframe.html
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Iframe window</title> </head> <body> <div>Iframe window</div> <script src='script.js'></script> </body> </html>
script.js
console.log('Hello from Iframe');
Решение
Общая идея состоит в том, что мы хотим, чтобы «большой» файл script.js кэшировался асинхронно при загрузке первой страницы index.html. При этом, когда мы нажимаем кнопку, script.js брался бы из кэша (быстро).
Ключом к этому решению является использование service worker
Код решения
Сначала мы добавляем код загрузчика service worker в…
app.js
var openButtonEl = document.getElementById('open-button'); openButtonEl.addEventListener('click', handleClick); if ('serviceWorker' in navigator) { navigator.serviceWorker.register('worker.js').then( handleRegisterSuccess, handleRegisterError, ); } function handleClick() { openButtonEl.setAttribute('disabled', true); var iframeEl = document.createElement('iframe'); iframeEl.src = 'iframe.html'; document.body.appendChild(iframeEl); } function handleRegisterSuccess(registration) { console.log('ServiceWorker registration successful with scope: ', registration.scope); } function handleRegisterError(err) { console.log('ServiceWorker registration failed: ', err); }
и создадим код service worker …
worker.js
console.log('LOAD'); var CACHE_NAME = 'cache'; var URLS = [ 'script.js', ]; self.addEventListener('install', handleInstall); self.addEventListener('activate', handleActivate); self.addEventListener('fetch', handleFetch); function handleInstall(event) { console.log('INSTALL'); event.waitUntil( caches.open(CACHE_NAME).then(handleCacheOpen) ); function handleCacheOpen(cache) { console.log('CACHE OPEN'); return cache.addAll(URLS); } } function handleActivate() { console.log('ACTIVATE'); } function handleFetch(event) { console.log('FETCH'); console.log(event.request.url); if (event.request.url === 'http://localhost:8080/') { console.log('IGNORE HOST PAGE'); return; } event.respondWith( caches.match(event.request).then(handleMatch) ); function handleMatch(response) { console.log('MATCH'); console.log(response); return response || fetch(event.request); } }
Решение в действии
Во-первых, чтобы избежать путаницы между кэшем браузера и кешем service worker, я отключил кэш браузера, настроив сервер на немедленное истечение срока действия контента; используя http-server.
npx http-server -c-1
Используя вкладку «Network» в Chrome Developer Tools, мы видим, что при первоначальной загрузке index.html, worker.js загружается асинхронно…
… И запускает service worker (как видно на вкладке «Application»)
service worker, в свою очередь, загружает script.js в кеш; см. вкладку «Network» выше и вкладку «Application» ниже.
Когда мы затем нажимаем кнопку, index.html получает iframe.html от service worker (который, в свою очередь, получает его от веб-сервера); как видно на вкладке Network.
С другой стороны, index.html получает script.js от service worker (который, в свою очередь, получает его из кэша service worker );