'why is onupdatefound method not getting triggered in dockerised PWA react app
I am working on an dockerised app whose frontend is built with create-react-app.
Note that this app was NOT built using pwa template. i.e, using npx create-react-app --template cra-template-pwa
hence this does not has workbox implementation.
Here is the folder structure of the project
--apps
|__api
|__frontend
|__public
|__index.html
|__manifest.json
|__service-worker.js
|__src
|__App.js
|__index.js
|__serviceWorkerRegistration.js
Here is the how files are
public/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#fff">
<meta name="apple-mobile-web-app-status-bar-style" content="#fff">
<meta name="facebook-domain-verification" content="yb2nwega2uk10tkg60916ofwqu02xx">
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
<style>
#snackbar {
visibility: hidden;
min-width: 250px;
margin-left: -125px;
background-color: #333;
color: #fff;
text-align: center;
border-radius: 2px;
padding: 16px;
position: fixed;
z-index: 1;
left: 50%;
bottom: 30px;
}
#snackbar.show {
visibility: visible;
-webkit-animation: fadein 0.5s;
animation: fadein 0.5s;
}
@-webkit-keyframes fadein {
from {
bottom: 0;
opacity: 0;
}
to {
bottom: 30px;
opacity: 1;
}
}
@keyframes fadein {
from {
bottom: 0;
opacity: 0;
}
to {
bottom: 30px;
opacity: 1;
}
}
</style>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<div id="snackbar">A new version of this app is available. Click <a id="reload">here</a> to update.</div>
</body>
</html>
public/service-worker.js
/* eslint no-restricted-globals: 0 */
/* eslint array-callback-return: 0 */
const HTMLToCache = '/';
const version = 'MSW V0.3';
self.addEventListener('install', event => {
event.waitUntil(
caches.open(version).then(cache => {
cache.add(HTMLToCache).then(self.skipWaiting());
})
);
});
self.addEventListener('message', event => {
if (event.data && event.data.action === 'skipWaiting') {
self.skipWaiting();
}
});
self.addEventListener('activate', event => {
event.waitUntil(
caches
.keys()
.then(cacheNames =>
Promise.all(
cacheNames.map(cacheName => {
if (version !== cacheName) return caches.delete(cacheName);
})
)
)
.then(self.clients.claim())
);
});
self.addEventListener('fetch', event => {
const requestToFetch = event.request.clone();
event.respondWith(
caches.match(event.request.clone()).then(cached => {
// We don't return cached HTML (except if fetch failed)
if (cached) {
const resourceType = cached.headers.get('content-type');
// We only return non css/js/html cached response e.g images
if (!hasHash(event.request.url) && !/text\/html/.test(resourceType)) {
return cached;
}
// If the CSS/JS didn't change since it's been cached, return the cached version
if (
hasHash(event.request.url) &&
hasSameHash(event.request.url, cached.url)
) {
return cached;
}
}
return fetch(requestToFetch)
.then(response => {
const clonedResponse = response.clone();
const contentType = clonedResponse.headers.get('content-type');
if (
!clonedResponse ||
clonedResponse.status !== 200 ||
clonedResponse.type !== 'basic' ||
/\/sockjs\//.test(event.request.url)
) {
return response;
}
if (/html/.test(contentType)) {
caches
.open(version)
.then(cache => cache.put(HTMLToCache, clonedResponse));
} else {
// Delete old version of a file
if (hasHash(event.request.url)) {
caches.open(version).then(cache =>
cache.keys().then(keys =>
keys.forEach(asset => {
if (
new RegExp(removeHash(event.request.url)).test(
removeHash(asset.url)
)
) {
cache.delete(asset);
}
})
)
);
}
caches.open(version).then(cache => {
if (event.request.url.indexOf('http') !== 0) return;
cache.put(event.request, clonedResponse);
});
}
return response;
})
.catch(() => {
if (hasHash(event.request.url))
return caches.match(event.request.url);
// If the request URL hasn't been served from cache and isn't sockjs we suppose it's HTML
else if (!/\/sockjs\//.test(event.request.url))
return caches.match(HTMLToCache);
// Only for sockjs
return new Response('No connection to the server', {
status: 503,
statusText: 'No connection to the server',
headers: new Headers({ 'Content-Type': 'text/plain' }),
});
});
})
);
});
function removeHash(element) {
if (typeof element === 'string') return element.split('?hash=')[0];
}
function hasHash(element) {
if (typeof element === 'string') return /\?hash=.*/.test(element);
}
function hasSameHash(firstUrl, secondUrl) {
if (typeof firstUrl === 'string' && typeof secondUrl === 'string') {
return /\?hash=(.*)/.exec(firstUrl)[1] === /\?hash=(.*)/.exec(secondUrl)[1];
}
}
src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import * as serviceWorkerRegistration from './serviceWorkerRegistration';
import { BrowserRouter as Router } from 'react-router-dom';
import { Provider } from 'react-redux';
import { configureStore } from './store';
import { StylesProvider } from '@material-ui/styles';
import App from './App';
const store = configureStore();
const AppContainer = (
<Provider store={store}>
<StylesProvider injectFirst>
<Router>
<App />
</Router>
</StylesProvider>
</Provider>
);
ReactDOM.render(AppContainer, document.getElementById('root'));
serviceWorkerRegistration.register();
src/serviceWorkerRegistration.js
// This optional code is used to register a service worker.
// register() is not called by default.
// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on subsequent visits to a page, after all the
// existing tabs open on the page have been closed, since previously cached
// resources are updated in the background.
// To learn more about the benefits of this model and instructions on how to
// opt-in, read https://cra.link/PWA
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}$/
)
);
export function register(config) {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
return;
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.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. To learn more, visit https://cra.link/PWA'
);
});
} else {
// Is not localhost. Just register service worker
registerValidSW(swUrl, config);
}
});
}
}
function registerValidSW(swUrl, config) {
let newWorker;
function showUpdateBar() {
let snackbar = document.getElementById('snackbar');
snackbar.className = 'show';
}
// The click event on the pop up notification
document.getElementById('reload').addEventListener('click', function(event) {
newWorker.postMessage({ action: 'skipWaiting' });
});
if ('serviceWorker' in navigator) {
let refreshing;
navigator.serviceWorker.addEventListener('controllerchange', function() {
if (refreshing) return;
window.location.reload();
refreshing = true;
});
}
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
newWorker = registration.installing;
console.log('update found');
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.'
);
showUpdateBar();
// Execute callback
if (config && config.onUpdate) {
console.log('on update');
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, 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.'
);
});
}
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready
.then(registration => {
registration.unregister();
})
.catch(error => {
console.error(error.message);
});
}
}
I have implemented PWA but something seems wrong. There are two things that I want to improve in the PWA
- Show a banner that lets user update the app whenever a new feature / bugfix is added to app although I never see
onupdatefoundlistener getting triggered. - Improve overall performance because PWA seems to slow down the API services
I'm wondering where am I going to wrong. Any help would be highly appreciated
Note: Things I have tried:
- creating a new react app using
npx create-react-app --template cra-template-pwa, copying service-worker.js insrc/service-worker.jsand installing allworkbox-*dependencies This approach does not work because theservice-worker.jsfile is insrcand not inpublicfolder and hence I see
Site cannot be installed: no matching service worker detected. You may need to reload the page, or check that the service worker for the current page also controls the start URL from the manifest
Something is off with how things are being bundled using webpack in the project that was built using --template cra-template-pwa vs simple project
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|
