VitePWA Как создать offline.html страницу?

Я пишу игру https://colobit.ru на react+ts+vite. Чтобы игру можно было установить, как нативное приложение, я использую плагин vitePWA и workbox. Недавно я сделал из приложения с помощью TWA apk файл и хотел опубликовать игру в RuStore, но там нельзя этого делать, тк я использую webView без поддержки оффлайн

Как мне добавить оффлайн поддержку? Я много раз пытался сделать это сам, но для меня это какая-то нереальная задача. Вот ни при каких условиях не работает у меня код, который должен в оффлайн работать.

Вот мой vite.config.ts

import {defineConfig} from 'vite';
import laravel from 'laravel-vite-plugin';
import react from '@vitejs/plugin-react';

import path from 'path';
import {VitePWA} from "vite-plugin-pwa";

const manifest = {
    "display": "standalone",
    "scope": "/",
    "start_url": "/farm",
    "name": "ColorBit",
    // ...
    "icons": [
        {
            "src": "icons/icon-192x192.png",
            "sizes": "192x192",
            "type": "image/png"
        },
        // ...
    ]
};

const getCache = ({name, pattern, strategy = "CacheFirst"}: any) => ({
    urlPattern: pattern,
    handler: strategy,
    options: {
        cacheName: name,
        expiration: {
            maxEntries: 500,
            maxAgeSeconds: 60 * 60 * 24 * 60 // 2 months
        },
        cacheableResponse: {
            statuses: [0, 200]
        }
    }
});

export default defineConfig({
    plugins: [
        laravel({
            input: ['resources/js/app.tsx',],
            refresh: true,
        }),
        react({
            fastRefresh: false
        }),
        VitePWA({
            registerType: 'autoUpdate',
            outDir: path.resolve(__dirname, 'public'),
            manifest: manifest,
            manifestFilename: 'manifest.webmanifest', // Change name for app manifest
            injectRegister: false, // I register SW in app.ts, disable auto registration

            srcDir: path.resolve(__dirname, 'resources/js/'),
            filename: 'serviceWorker.js',
            strategies: 'injectManifest',

            workbox: {
                globDirectory: path.resolve(__dirname, 'public'), // Directory for caching
                globPatterns: [
                    '{build,images,sounds,icons}/**/*.{js,css,html,ico,png,jpg,mp4,svg}'
                ],
            },
        })
    ],
    resolve: {
        alias: {
            '@': path.resolve(__dirname, 'resources/js'),
            // ...
        },
        extensions: ['.js', '.ts', '.tsx', '.jsx'],
    },

});

serviceWorker.js

import {ExpirationPlugin} from 'workbox-expiration';
import {createHandlerBoundToURL, precacheAndRoute, cleanupOutdatedCaches} from 'workbox-precaching';
import {registerRoute} from 'workbox-routing';
import {CacheFirst} from 'workbox-strategies';
import { CacheableResponsePlugin } from 'workbox-cacheable-response/CacheableResponsePlugin';

// Register precache routes (static cache)
precacheAndRoute(self.__WB_MANIFEST || []);

// Clean up old cache
cleanupOutdatedCaches();

// Google fonts dynamic cache
registerRoute(
    /^https:\/\/fonts\.googleapis\.com\/.*/i,
    new CacheFirst({
        cacheName: "google-fonts-cache",
        plugins: [
            new ExpirationPlugin({maxEntries: 500, maxAgeSeconds: 5184e3}),
            new CacheableResponsePlugin({statuses: [0, 200]})
        ]
    }), "GET");

// Google fonts dynamic cache
registerRoute(
    /^https:\/\/fonts\.gstatic\.com\/.*/i, new CacheFirst({
        cacheName: "gstatic-fonts-cache",
        plugins: [
            new ExpirationPlugin({maxEntries: 500, maxAgeSeconds: 5184e3}),
            new CacheableResponsePlugin({statuses: [0, 200]})
        ]
    }), "GET");

// Dynamic cache for images from `/storage/`
registerRoute(
    /.*storage.*/, new CacheFirst({
        cacheName: "dynamic-images-cache",
        plugins: [
            new ExpirationPlugin({maxEntries: 500, maxAgeSeconds: 5184e3}),
            new CacheableResponsePlugin({statuses: [0, 200]})
        ]
    }), "GET");


// Install and activate service worker
self.addEventListener('install', () => self.skipWaiting());
self.addEventListener('activate', () => self.clients.claim());

// Receive push notifications
self.addEventListener('push', function (e) {
    if (!(
        self.Notification &&
        self.Notification.permission === 'granted'
    )) {
        // notifications aren't supported or permission not granted!
        console.log('Notifications aren\'t supported or permission not granted!')
        return;
    }

    if (e.data) {
        let message = e.data.json();
        e.waitUntil(self.registration.showNotification(message.title, {
            body: message.body,
            icon: message.icon,
            actions: message.actions
        }));
    }
});

// Click and open notification
self.addEventListener('notificationclick', function(event) {
    event.notification.close();

    if (event.action === 'farm') clients.openWindow("/farm");
    // ...
    else clients.openWindow('/farm'); // Open link from action
}, false);

Нужные файлы действительно кэшируется, но когда я включаю оффлайн режим, у меня стандартная заглука от браузера.

введите сюда описание изображения введите сюда описание изображения

Как сделать так, чтобы при отсутствии интернета пользователю возвращалась страница offline.html? То есть https://colobit.ru/offline.html


Ответы (1 шт):

Автор решения: Color kat

Методом проб и ошибок удалось решить проблему. Вот мой service-worker.js

import {ExpirationPlugin} from 'workbox-expiration';
import {createHandlerBoundToURL, precacheAndRoute, cleanupOutdatedCaches} from 'workbox-precaching';
import {registerRoute} from 'workbox-routing';
import {CacheFirst} from 'workbox-strategies';
import { CacheableResponsePlugin } from 'workbox-cacheable-response/CacheableResponsePlugin';

// Register precache routes (static cache)
precacheAndRoute(self.__WB_MANIFEST || []);

// Clean up old cache
cleanupOutdatedCaches();

// Google fonts dynamic cache
registerRoute(
    /^https:\/\/fonts\.googleapis\.com\/.*/i,
    new CacheFirst({
        cacheName: "google-fonts-cache",
        plugins: [
            new ExpirationPlugin({maxEntries: 500, maxAgeSeconds: 5184e3}),
            new CacheableResponsePlugin({statuses: [0, 200]})
        ]
    }), "GET");

// Dynamic cache for images from `/storage/`
registerRoute(
    /.*storage.*/, new CacheFirst({
        cacheName: "dynamic-images-cache",
        plugins: [
            new ExpirationPlugin({maxEntries: 500, maxAgeSeconds: 5184e3}),
            new CacheableResponsePlugin({statuses: [0, 200]})
        ]
    }), "GET");


// Install and activate service worker
self.addEventListener('activate', () => self.clients.claim());

const offlineCacheName = 'offline-cache';
const offlineCacheFiles = [
    '/offline/offline.html',
    '/offline/error.png'
]
self.addEventListener('install', (event) => {
    event.waitUntil(
        caches.open(offlineCacheName).then((cache) => {
            return cache.addAll(offlineCacheFiles);
        })
    );
});

// Fetch event
self.addEventListener('fetch', (event) => {
    event.respondWith(
        caches.match(event.request).then(async (response) => {
            try {
                // If there's a cache response, return it
                // Or try to fetch it using the Internet
                return response || fetch(event.request).catch(() => {
                    // If the request fails (offline), return the offline.html page
                    return caches.match('/offline/offline.html');
                });
            } catch (e) {
                return caches.match('/offline/offline.html');
            }
        })
    );
});

→ Ссылка