/offline/
and generate a new page directly on the device, without a request to the server./service-worker.js
. In our case, this is required. If you give a service worker file from the /js/
directory, for example /js/service-worker.js
, then it will be able to process only those network requests that start with /js/
... // app.js - // service worker if ('serviceWorker' in navigator) { window.addEventListener('load', registerServiceWorker); } function registerServiceWorker () { // , // navigator.serviceWorker.register('/service-worker.js') .then(registration => { if (!registration.active) { // return; } // - , . // . }); }
/offline/
, all styles, images, etc. We pre- cache them with the install
event, the first one in the chain of life cycle events. // service-worker.js // , const dependencies = [ '/css/app.css', '/js/offline_page.js', '/img/logo.png', '/img/default_thumb.png' ]; // , - self.addEventListener('install', event => { // , offline- const loadDependencies = self.caches.open('myApp') .then(cache => cache.addAll(dependencies)); // - , // event.waitUntil(loadDependencies); });
activate
event. It is useful to us in order to clear the old cache and records in the database. In our example, a simple idb-keyval helper is used to work with IndexedDB . He and his more advanced idb brother are convenient wrappers that allow to work with the morally outdated API IndexedDB . // service-worker.js import { clear } from 'idb-keyval'; // , const dependencies = [/* ... */]; // self.addEventListener('activate', event => { // IndexedDB const promiseClearIDB = clear(); // const promiseClearCache = self.caches.open(cacheName) .then((cache) => cache.keys() .then((cacheKeys) => Promise.all(cacheKeys.map((request) => { // , , // const canDelete = !dependencies.includes(request.url); return canDelete ? cache.delete(request, {ignoreVary: true}) : Promise.resolve(); })))); const promiseClearAll = Promise.all([promiseClearIDB, promiseClearCache]) .catch(err => console.error(error)); // - , // IndexedDB event.waitUntil(promiseClearAll); });
/offline/
. // service-worker.js // self.addEventListener('fetch', event => { const { request } = event; const url = new URL(request.url); if (url.origin !== self.location.origin) { // , return fetch(request).catch(err => console.log(err)); } // , /offline/ const isOfflineListRequested = /^\/offline\//.test(url.pathname); const response = isOfflineListRequested // response // , ? createOfflineListResponse() // . // // , - " ", // - .. : fetchWithOneOrAnotherCacheStrategy(event.request); event.respondWith(response); });
postMessage
.head
block and receive it from the layout. Or in any other suitable way for you. <meta property="og:title" content="Homer Simpson" /> <meta property="og:url" content="http://example.com/homer.html" /> <meta property="og:image" content="http://example.com/homer.png" />
/homer.html
, /homer.html?utm_source=vk
and /homer.html?utm_source=email
actually lead to the same page, which means they must be registered in the list once. Here the “canonical” address transmitted via og:url
will help us, it will always be the same. Most likely you already have all the necessary og-markup, you can check its completeness with the help of Google Chrome extension .registerServiceWorker
function (see above). // app.js - function registerServiceWorker () { navigator.serviceWorker.register('/service-worker.js') .then(registration => { if (!registration.active) { // return; } // - , // , registerPageAsCached(); }); } /** * , */ function registerPageAsCached () { // getPageInfoFromHtml , // , : // url - "" // title - // thumb - const page = getPageInfoFromHtml(); // - postMessage({ action: 'registerPage', page }); } /** * service worker * @param {object} message */ function postMessage (message) { const {controller} = navigator.serviceWorker; if (controller) { controller.postMessage(message); } }
action
field describing the type of the message. This will allow us in the future to transmit different data for different purposes. // service-worker.js import {get, set} from 'idb-keyval'; /* * */ self.addEventListener('message', event => { const {data = {}} = event; const {page} = data; // , // , action switch (data.action) { case 'registerPage': addToOfflineList(page); break; } }); /** * , * @param {object} pageInfo * @return {Promise} */ export function addToOfflineList (pageInfo) { // , , // offline if (pageInfo.thumb) { fetchWithOneOrAnotherCacheStrategy(pageInfo.thumb); } // IndexedDB return get('cachedPages') .then((pages = {}) => set('cachedPages', { ...pages, [pageInfo.url]: pageInfo })); }
/offline/
, which we will do later. Also, for convenience, we will highlight the available links directly on the page.ping
function that will periodically be called at a specified interval and send a message to the service worker. // app.js - const PING_INTERVAL = 10000; // 10 function registerServiceWorker () { navigator.serviceWorker.register('/service-worker.js') .then(registration => { if (!registration.active) { // return; } // - , registerPageAsCached(); // . // ping(); }); } /** * ( ) */ function ping() { postMessage({ action: 'ping', }); setTimeout(ping, PING_INTERVAL); } /** * service worker * @param {object} message */ function postMessage (message) { const {controller} = navigator.serviceWorker; if (controller) { controller.postMessage(message); } }
// service-worker.js import {get} from 'idb-keyval'; /* * */ self.addEventListener('message', event => { const {data = {}} = event; const {page} = data; // , // , action switch (data.action) { case 'ping': ping(); break; } }); /** * */ export function ping () { fetch('/ping.gif').then( () => pingHandler(true), () => pingHandler(false) ); } /** * * * @param {boolean} isOnline */ function pingHandler (isOnline) { postMessage({ action: 'ping', online: isOnline, }); } /** * , * - * @param {object} message */ function postMessage (message) { // self.clients.matchAll().then(clients => { // // const offlinePagesPromise = message.online ? Promise.resolve() : get('cachedPages'); offlinePagesPromise.then(offlinePages => { if (offlinePages) { message.offlinePages = offlinePages; } clients.forEach(client => { // , if (client) { client.postMessage(message); } }); }); }); }
ping
method. Therefore, it is better not to load a pixel from the page, but through a service worker who can control the frequency of network check requests, for example, through the throttle micro pattern . Also, the knowledge of the status can be useful to the service worker himself. // app.js - let isOnline = true; function registerServiceWorker () { navigator.serviceWorker.register('/service-worker.js') .then(registration => { if (!registration.active) { // return; } // - serviceWorker.addEventListener('message', handleMessage); registerPageAsCached(); // . ping(); // . }); } /** * service worker- * @param {MessageEvent} e */ function handleMessage (e) { const {data} = e; if (data.action === 'ping' && isOnline !== data.online) { isOnline = data.online; toggleNetworkState(data); } } /** * / * @param {object} params */ function toggleNetworkState (params) { const {online, offlinePages = {}} = params; // , // "" document.body.classList.toggle('offline', !online); // if (!online) { Array.from(document.links).forEach(link => { const href = link.getAttribute('href'); const isCached = !!offlinePages[href] || href === '/offline/'; link.classList.toggle('cached', isCached); }); } }
html(lang="en") head title Available offline link(rel="stylesheet" href="/css/app.css") body section.layout header.layout__header a.layout__header__logo(href="/") h1 You can read it offline ul.articles-list each page in pages li.articles-list__item a(href=page.url) if page.thumb img.avatar(src=page.thumb alt="") else img.avatar(src="/img/default_thumb.png" alt="") span=page.title
fetch
event handler, select the request to /offline/
and return the newly created page to the unsuspecting browser: // service-worker.js import {get} from 'idb-keyval'; const template = require('offlinePage.pug'); // self.addEventListener('fetch', event => { const { request } = event; const url = new URL(request.url); if (url.origin !== self.location.origin) { // , return fetch(request).catch(err => console.log(err)); } // , /offline/ const isOfflineListRequested = /^\/offline\//.test(url.pathname); const response = isOfflineListRequested // ! // response // , ? createOfflineListResponse() // : fetchWithOneOrAnotherCacheStrategy(event.request); event.respondWith(response); }); /** * response , * * @return {Promise<Response>} */ function createOfflineListResponse () { // return get('cachedPages') .then((pagesList = {}) => { // const html = template({ pages: Object.values(pagesList) }); // const blob = new Blob([html], { type: 'text/html; charset=utf-8' }); return new Response(blob, { status: 200, statusText: 'OK' }); }).catch(err => console.error(err)); }
log
, which inside is able to turn on / off the flag from the environment, and display information through it. Unlike the pages, the service worker continues to live between their reloads and closing, so I recommend turning on the Preserve log
checkbox in the developer’s tools console.Source: https://habr.com/ru/post/353232/
All Articles