Skip to content

Instantly share code, notes, and snippets.

@gugadev
Last active April 9, 2022 22:47
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save gugadev/b2dfa3f2cec761069af7a67026a78817 to your computer and use it in GitHub Desktop.
Save gugadev/b2dfa3f2cec761069af7a67026a78817 to your computer and use it in GitHub Desktop.
Service worker examples in TypeScript and JavaScript
/**
* * 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();
{
"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"
}
}
/**
* ! 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."
);
});
}
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.");
}
}
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();
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