Skip to content

Instantly share code, notes, and snippets.

@gugadev
Last active April 9, 2022 22:47
Show Gist options
  • Select an option

  • Save gugadev/b2dfa3f2cec761069af7a67026a78817 to your computer and use it in GitHub Desktop.

Select an option

Save gugadev/b2dfa3f2cec761069af7a67026a78817 to your computer and use it in GitHub Desktop.

Revisions

  1. gugadev revised this gist Apr 9, 2022. 2 changed files with 13 additions and 10 deletions.
    8 changes: 4 additions & 4 deletions sw.js
    Original file line number Diff line number Diff line change
    @@ -134,11 +134,11 @@ if ("function" === typeof importScripts) {
    if (event.request.url.includes("aec-notifications")) return;
    if (event.request.url.includes("chrome-extension")) return;

    fetchHandler(event).then((response) => {
    event.respondWith(response);
    }).catch(err => {
    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)
    15 changes: 9 additions & 6 deletions sw.ts
    Original file line number Diff line number Diff line change
    @@ -7,12 +7,14 @@ 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!");
    workbox.setConfig({ debug: true });
    // ? No se puede establecer el modo de bug así :p
    //workbox.setConfig({ debug: true });
    }

    // limpiamos el caché viejo
    @@ -107,6 +109,7 @@ registerRoute(
    );

    /**
    * ! Importante
    * Este método almacena en caché las peticiones ajax que se hagan.
    */
    const fetchHandler = async (event: FetchEvent) => {
    @@ -135,12 +138,12 @@ self.addEventListener("fetch", (event: FetchEvent) => {
    if (event.request.url.includes("aec-notifications")) return;
    if (event.request.url.includes("chrome-extension")) return;

    fetchHandler(event).then((response) => {
    event.respondWith(response);
    }).catch(err => {
    try {
    event.respondWith(fetchHandler(event));
    } catch (err) {
    console.warn(`Falló la petición a "${event.request.url}:"`, err);
    });
    }
    });

    // Offline Google Analytics (opcional)
    workbox.googleAnalytics.initialize();
    initializeGA();
  2. gugadev created this gist Apr 9, 2022.
    29 changes: 29 additions & 0 deletions build-sw.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,29 @@
    /**
    * * 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();
    9 changes: 9 additions & 0 deletions package.json
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,9 @@
    {
    "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"
    }
    }
    139 changes: 139 additions & 0 deletions sw-manager.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,139 @@
    /**
    * ! 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."
    );
    });
    }

    151 changes: 151 additions & 0 deletions sw.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,151 @@
    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;

    fetchHandler(event).then((response) => {
    event.respondWith(response);
    }).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.");
    }
    }
    146 changes: 146 additions & 0 deletions sw.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,146 @@
    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";

    declare const self: ServiceWorkerGlobalScope;

    if (self && self.location && self.location.hostname === "localhost") {
    console.log("Localhost detected. Running Workbox in debug mode!");
    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] })],
    })
    );

    /**
    * 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;

    fetchHandler(event).then((response) => {
    event.respondWith(response);
    }).catch(err => {
    console.warn(`Falló la petición a "${event.request.url}:"`, err);
    });
    });

    // Offline Google Analytics (opcional)
    workbox.googleAnalytics.initialize();
    8 changes: 8 additions & 0 deletions workbox.config.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,8 @@
    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"],
    };