Last active
April 9, 2022 22:47
Service worker examples in TypeScript and JavaScript
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* * This is not in use. | |
* * Para conocimiento general: | |
* Esta es una forma de generar el "precache manifest" | |
* de un service worker. La otra forma es como se detalla | |
* en el script de NPM "sw" en el presente package.json. | |
*/ | |
const workboxBuild = require("workbox-build"); | |
const buildServiceWorker = () => { | |
return workboxBuild | |
.injectManifest({ | |
swSrc: "src/sw.js", // service worker personalizado | |
swDest: "dist/sw.js", // service worker generado por Workbox. | |
globDirectory: "dist", | |
globPatterns: ["**/*.{js,css,html,png,svg}"], | |
maximumFileSizeToCacheInBytes: 5 * 1024 * 1024, // 5mb | |
globIgnores: ["**/*.map", "**/asset-manifest*.js", "**/sw.js"] | |
}) | |
.then(({ count, size, warnings }) => { | |
warnings.forEach(console.warn); | |
console.info(`${count} archivos serán precacheados. Total: ${size / (1024 * 1024)} MBs.`); | |
}) | |
.catch(err => { | |
console.warn(`Error al inyectar service worker: ${err}`); | |
}); | |
}; | |
buildServiceWorker(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"scripts": { | |
"build": "<your framework script here> && npm run sw", | |
"build-sw": "node scripts/sw-build.js", | |
"compile-sw": "esbuild --outfile=src/sw.js --bundle src/sw.ts", | |
"inject-sw": "workbox injectManifest workbox-config.js", | |
"sw": "npm run compile-sw && npm run inject-sw" | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* ! Importante | |
* * Este archivo se encarga de registrar / suprimir | |
* * el service worker presente. | |
*/ | |
const isLocalhost = Boolean( | |
window.location.hostname === "localhost" || | |
// [::1] is the IPv6 localhost address. | |
window.location.hostname === "[::1]" || | |
// 127.0.0.0/8 are considered localhost for IPv4. | |
window.location.hostname.match( | |
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ | |
) | |
); | |
type Config = { | |
onSuccess?: (registration: ServiceWorkerRegistration) => void; | |
onUpdate?: (registration: ServiceWorkerRegistration) => void; | |
}; | |
export function register(env: Record<string, any>, config?: Config): void { | |
if ("serviceWorker" in navigator) { | |
// No debe funcionar en otro dominio que el que definamos en la property | |
const publicUrl = new URL(env.VITE_APP_URL!, window.location.href); | |
if (publicUrl.origin !== window.location.origin) { | |
return; | |
} | |
window.addEventListener("load", () => { | |
const swUrl = `${window.location.origin}/sw.js`; | |
if (isLocalhost) { | |
// This is running on localhost. Let's check if a service worker still exists or not. | |
checkValidServiceWorker(swUrl, config); | |
// Add some additional logging to localhost, pointing developers to the | |
// service worker/PWA documentation. | |
navigator.serviceWorker.ready.then(() => { | |
console.log( | |
"This web app is being served cache-first by a service " + | |
"worker." | |
); | |
}); | |
} else { | |
// Is not localhost. Just register service worker | |
registerValidSW(swUrl, config); | |
} | |
}); | |
} | |
} | |
export function unregister(): void { | |
if ("serviceWorker" in navigator) { | |
navigator.serviceWorker.ready | |
.then((registration) => { | |
registration.unregister(); | |
}) | |
.catch((error) => { | |
console.error(error.message); | |
}); | |
} | |
} | |
function registerValidSW(swUrl: string, config?: Config) { | |
navigator.serviceWorker | |
.register(swUrl) | |
.then((registration) => { | |
registration.onupdatefound = () => { | |
const installingWorker = registration.installing; | |
if (installingWorker == null) { | |
return; | |
} | |
installingWorker.onstatechange = () => { | |
if (installingWorker.state === "installed") { | |
if (navigator.serviceWorker.controller) { | |
// At this point, the updated precached content has been fetched, | |
// but the previous service worker will still serve the older | |
// content until all client tabs are closed. | |
console.log( | |
"New content is available and will be used when all " + | |
"tabs for this page are closed. See https://cra.link/PWA." | |
); | |
// Execute callback | |
if (config && config.onUpdate) { | |
config.onUpdate(registration); | |
} | |
} else { | |
// At this point, everything has been precached. | |
// It's the perfect time to display a | |
// "Content is cached for offline use." message. | |
console.log("Content is cached for offline use."); | |
// Execute callback | |
if (config && config.onSuccess) { | |
config.onSuccess(registration); | |
} | |
} | |
} | |
}; | |
}; | |
}) | |
.catch((error) => { | |
console.error("Error during service worker registration:", error); | |
}); | |
} | |
function checkValidServiceWorker(swUrl: string, config?: Config) { | |
// Check if the service worker can be found. If it can't reload the page. | |
fetch(swUrl, { | |
headers: { "Service-Worker": "script" }, | |
}) | |
.then((response) => { | |
// Ensure service worker exists, and that we really are getting a JS file. | |
const contentType = response.headers.get("content-type"); | |
if ( | |
response.status === 404 || | |
(contentType != null && | |
contentType.indexOf("javascript") === -1) | |
) { | |
// No service worker found. Probably a different app. Reload the page. | |
navigator.serviceWorker.ready.then((registration) => { | |
registration.unregister().then(() => { | |
window.location.reload(); | |
}); | |
}); | |
} else { | |
// Service worker found. Proceed as normal. | |
registerValidSW(swUrl, config); | |
} | |
}) | |
.catch(() => { | |
console.log( | |
"No internet connection found. App is running in offline mode." | |
); | |
}); | |
} | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
if ("function" === typeof importScripts) { | |
importScripts( | |
"https://storage.googleapis.com/workbox-cdn/releases/5.1.2/workbox-sw.js" | |
); | |
// Workbox loaded | |
if (workbox) { | |
if (self && self.location && self.location.hostname === "localhost") { | |
console.log("Localhost detected. Running Workbox in debug mode!"); | |
workbox.setConfig({ debug: true }); | |
} | |
const { registerRoute, NavigationRoute } = workbox.routing; | |
const { CacheFirst, StaleWhileRevalidate } = workbox.strategies; | |
const { CacheableResponsePlugin } = workbox.cacheableResponse; | |
const { RangeRequestsPlugin } = workbox.rangeRequests; | |
const { | |
cleanupOutdatedCaches, | |
precacheAndRoute, | |
createHandlerBoundToURL, | |
} = workbox.precaching; | |
// limpiamos el caché viejo | |
cleanupOutdatedCaches() | |
// Manifest injection point | |
precacheAndRoute(self.__WB_MANIFEST); | |
/** | |
* Aquí empieza nuestra configuración | |
**/ | |
/** | |
* ! Fallback a /index.html, ya que es una SPA, | |
* ! pero para los casos de 2020 y 2021, | |
* ! no hacemos efectivo este fallback | |
**/ | |
const indexRoute = new NavigationRoute(createHandlerBoundToURL("/index.html"), { | |
denylist: [ | |
/^\/__/, /\/[^\/]+.[^\/]+$/, | |
new RegExp("/2020/"), | |
new RegExp("/2021/") | |
], | |
}); | |
registerRoute(indexRoute); | |
registerRoute( | |
({ request }) => request.destination === "image", | |
new StaleWhileRevalidate({ | |
cacheName: "aec-images", | |
plugins: [new CacheableResponsePlugin({ statuses: [0, 200] })], | |
}) | |
); | |
registerRoute( | |
({ request }) => request.destination === "font", | |
new StaleWhileRevalidate({ | |
cacheName: "aec-fonts", | |
plugins: [new CacheableResponsePlugin({ statuses: [0, 200] })], | |
}) | |
); | |
registerRoute( | |
({ request }) => request.destination === "style", | |
new StaleWhileRevalidate({ | |
cacheName: "aec-styles", | |
plugins: [new CacheableResponsePlugin({ statuses: [0, 200] })], | |
}) | |
); | |
registerRoute( | |
({request}) => { | |
const {destination} = request; | |
return destination === 'video' || destination === 'audio' | |
}, | |
new CacheFirst({ | |
cacheName: 'aec-multimedia', | |
plugins: [ | |
new CacheableResponsePlugin({ | |
statuses: [200] | |
}), | |
new RangeRequestsPlugin(), | |
], | |
}), | |
); | |
/** | |
* ! Importante | |
* Para los casos de 2020 y 2021, los estáticos (incluyendo HTML) | |
* son obtenidos de caché, pero por detrás hacen la petición para | |
* actualizar los archivos desde la red. | |
*/ | |
registerRoute( | |
({ url }) => url.pathname.startsWith("/2020"), | |
new StaleWhileRevalidate({ | |
plugins: [new CacheableResponsePlugin({ statuses: [0, 200] })], | |
}) | |
); | |
registerRoute( | |
({ url }) => url.pathname.startsWith("/2021"), | |
new StaleWhileRevalidate({ | |
plugins: [new CacheableResponsePlugin({ statuses: [0, 200] })], | |
}) | |
); | |
/** | |
* ! Importante | |
* Este método almacena en caché las peticiones ajax que se hagan. | |
*/ | |
const fetchHandler = async (event) => { | |
const cache = await caches.open("aec-api-v4"); | |
const cachedResponse = await cache.match(event.request); | |
if (cachedResponse) { | |
// si está en caché, refrescamos el valor | |
event.waitUntil(cache.add(event.request)); | |
// y devolvemos lo que está en caché | |
return cachedResponse; | |
} | |
// Si no encontramos una coincidencia en el caché, usa la red. | |
return fetch(event.request).then((response) => { | |
// para evitar errore de tipo "Failed to add/put" | |
if (response.status === 200) { | |
cache.put(event.request, response.clone()); | |
} | |
return response; | |
}); | |
} | |
self.addEventListener("fetch", (event) => { | |
if (event.request.method !== "GET") return; | |
if (event.request.url.includes("aec-notifications")) return; | |
if (event.request.url.includes("chrome-extension")) return; | |
try { | |
event.respondWith(fetchHandler(event)); | |
} catch (err) { | |
console.warn(`Falló la petición a "${event.request.url}:"`, err); | |
} | |
}); | |
// Offline Google Analytics (if you want it) | |
workbox.googleAnalytics.initialize(); | |
// You can fit other workbox modules and configure them how you want... | |
} else { | |
console.error(" Workbox could not be loaded. No offline support."); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
cleanupOutdatedCaches, | |
precacheAndRoute, | |
createHandlerBoundToURL, | |
} from "workbox-precaching"; | |
import { registerRoute, NavigationRoute } from "workbox-routing"; | |
import { CacheFirst, StaleWhileRevalidate } from "workbox-strategies"; | |
import { CacheableResponsePlugin } from "workbox-cacheable-response"; | |
import { RangeRequestsPlugin } from "workbox-range-requests"; | |
import { initialize as initializeGA } from "workbox-google-analytics"; | |
declare const self: ServiceWorkerGlobalScope; | |
if (self && self.location && self.location.hostname === "localhost") { | |
console.log("Localhost detected. Running Workbox in debug mode!"); | |
// ? No se puede establecer el modo de bug así :p | |
//workbox.setConfig({ debug: true }); | |
} | |
// limpiamos el caché viejo | |
cleanupOutdatedCaches() | |
// Aquí se inyectan las rutas que genera Workbox | |
precacheAndRoute(self.__WB_MANIFEST); | |
/** | |
* ! Importante | |
* Fallback a /index.html, ya que es una SPA, | |
* pero para los casos de 2020 y 2021, | |
* no hacemos efectivo este fallback | |
**/ | |
const indexRoute = new NavigationRoute(createHandlerBoundToURL("/index.html"), { | |
denylist: [ | |
/^\/__/, /\/[^\/]+.[^\/]+$/, | |
new RegExp("/2020/"), | |
new RegExp("/2021/") | |
], | |
}); | |
registerRoute(indexRoute); | |
registerRoute( | |
({ request }) => request.destination === "image", | |
new StaleWhileRevalidate({ | |
cacheName: "aec-images", | |
plugins: [new CacheableResponsePlugin({ statuses: [0, 200] })], | |
}) | |
); | |
registerRoute( | |
({ request }) => request.destination === "font", | |
new StaleWhileRevalidate({ | |
cacheName: "aec-fonts", | |
plugins: [new CacheableResponsePlugin({ statuses: [0, 200] })], | |
}) | |
); | |
registerRoute( | |
({ request }) => request.destination === "style", | |
new StaleWhileRevalidate({ | |
cacheName: "aec-styles", | |
plugins: [new CacheableResponsePlugin({ statuses: [0, 200] })], | |
}) | |
); | |
registerRoute( | |
({request}) => { | |
const {destination} = request; | |
return destination === 'video' || destination === 'audio' | |
}, | |
new CacheFirst({ | |
cacheName: 'aec-multimedia', | |
plugins: [ | |
new CacheableResponsePlugin({ | |
statuses: [200] | |
}), | |
new RangeRequestsPlugin(), | |
], | |
}), | |
); | |
registerRoute( | |
({ url }) => | |
url.origin === self.location.origin && | |
url.pathname.endsWith(".json"), | |
new CacheFirst({ | |
cacheName: "data", | |
plugins: [new CacheableResponsePlugin({ statuses: [0, 200] })], | |
}) | |
); | |
/** | |
* ! Importante | |
* Para los casos de 2020 y 2021, los estáticos (incluyendo HTML) | |
* son obtenidos de caché, pero por detrás hacen la petición para | |
* actualizar los archivos desde la red. | |
*/ | |
registerRoute( | |
({ url }) => url.pathname.startsWith("/2020"), | |
new StaleWhileRevalidate({ | |
plugins: [new CacheableResponsePlugin({ statuses: [0, 200] })], | |
}) | |
); | |
registerRoute( | |
({ url }) => url.pathname.startsWith("/2021"), | |
new StaleWhileRevalidate({ | |
plugins: [new CacheableResponsePlugin({ statuses: [0, 200] })], | |
}) | |
); | |
/** | |
* ! Importante | |
* Este método almacena en caché las peticiones ajax que se hagan. | |
*/ | |
const fetchHandler = async (event: FetchEvent) => { | |
const cache = await caches.open("aec-api-v4"); | |
const cachedResponse = await cache.match(event.request); | |
if (cachedResponse) { | |
// si está en caché, refrescamos el valor | |
event.waitUntil(cache.add(event.request)); | |
// y devolvemos lo que está en caché | |
return cachedResponse; | |
} | |
// Si no encontramos una coincidencia en el caché, usa la red. | |
return fetch(event.request).then((response) => { | |
// para evitar errore de tipo "Failed to add/put" | |
if (response.status === 200) { | |
cache.put(event.request, response.clone()); | |
} | |
return response; | |
}); | |
} | |
self.addEventListener("fetch", (event: FetchEvent) => { | |
if (event.request.method !== "GET") return; | |
if (event.request.url.includes("aec-notifications")) return; | |
if (event.request.url.includes("chrome-extension")) return; | |
try { | |
event.respondWith(fetchHandler(event)); | |
} catch (err) { | |
console.warn(`Falló la petición a "${event.request.url}:"`, err); | |
} | |
}); | |
// Offline Google Analytics (opcional) | |
initializeGA(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
module.exports = { | |
swSrc: "src/sw.js", | |
swDest: "dist/sw.js", // o build, depende de cómo se llame el folder de compilación | |
globDirectory: "dist", // o build, depende de cómo se llame el folder de compilación | |
globPatterns: ["**/*.{js,css,html,png,svg}"], | |
maximumFileSizeToCacheInBytes: 5 * 1024 * 1024, // 5mb | |
globIgnores: ["**/*.map", "**/asset-manifest*.js", "**/sw.js"], | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment