I am building a service worker for my PWA with Workbox.
service-worker.js
import { registerRoute } from 'workbox-routing';
import { NetworkFirst } from 'workbox-strategies';
import { CacheFirst, StaleWhileRevalidate } from 'workbox-strategies';
import { ExpirationPlugin } from 'workbox-expiration';
import * as googleAnalytics from 'workbox-google-analytics';
import { precacheAndRoute } from 'workbox-precaching';
googleAnalytics.initialize();
registerRoute(
({ request }) => request.destination === 'script',
new NetworkFirst()
);
registerRoute(
// Cache style resources, i.e. CSS files.
({ request }) => request.destination === 'style',
// Use cache but update in the background.
new StaleWhileRevalidate({
// Use a custom cache name.
cacheName: 'css-cache',
})
);
registerRoute(
// Cache image files.
({ request }) => request.destination === 'image',
// Use the cache if it's available.
new CacheFirst({
// Use a custom cache name.
cacheName: 'image-cache',
plugins: [
new ExpirationPlugin({
// Cache for a maximum of a week.
maxAgeSeconds: 7 * 24 * 60 * 60,
}),
],
})
);
precacheAndRoute(self.__WB_MANIFEST);
if (process.env.NODE_ENV !== 'production') {
console.log('This is a dev-only log message!');
}
Is there a way that Parcel injects only the latest version of files in the service worker after each build?
My Attempts
I tried using workbox-build in a node script but each time I ran npm run build, it added all the versions of the file present in the dist/ folder.
service-worker.js (when I used a Node script)
// code skipped for brevity
precacheAndRoute([
{"revision":"78416d1f083fa1a898d17e20d741a462","url":"Share.332aebe4.js"},{"revision":"413f2ed8c0dcd51e3c01855ac307f6d2","url":"Share.77f580e5.js"},{"revision":"c95daa634502ca4a38e1822e7460688e","url":"style.0f60499b.css"},{"revision":"7f4b6e94999540101a2a7e5b4226080e","url":"style.78032849.css"}])
They were different versions of the same file but I want only the latest version
I tried using parcel-plugin-sw-cache but couldn't figure out how to use it due to insufficient documentation.
parcel-plugin-sw-cache works when you change
precacheAndRoute(self.__WB_MANIFEST);
to
precacheAndRoute([]);
Also, when you're using injectManifest method, you need to provide "swSrc": "service-worker.js" in package.json (according to documentation)
Related
I have a service-worker.js file to make my reactjs app PWA. I now also want to add push notification using FCM, which requires me to have firebase-messaging-sw.js in the public folder. So now for both to work both are going to require to be in the same scope.
But as far as I have read from various answers on this site, we can't have two different service workers in the same scope, so how do we combine both service-worker.js and firebase-messaging-sw.js so both can function properly. One of the answers suggested that I rename the service-worker.js to firebase-messaging-sw.js which doesn't work. I did find one successful implementation on GitHub which I didn't understand much https://github.com/ERS-HCL/reactjs-pwa-firebase .
How can I have both the service-worker.js and firebase-messaging-sw.js work together?
firebase-messaging-sw.js
// Scripts for firebase and firebase messaging
importScripts("https://www.gstatic.com/firebasejs/8.2.0/firebase-app.js");
importScripts("https://www.gstatic.com/firebasejs/8.2.0/firebase-messaging.js");
// Initialize the Firebase app in the service worker by passing the generated config
const firebaseConfig = {
apiKey: "xxxx",
authDomain: "xxxx.firebaseapp.com",
projectId: "xxxx",
storageBucket: "xxxx",
messagingSenderId: "xxxx",
appId: "xxxx",
measurementId: "xxxx"
}
firebase.initializeApp(firebaseConfig);
// Retrieve firebase messaging
const messaging = firebase.messaging();
self.addEventListener("notificationclick", function (event) {
console.debug('SW notification click event', event)
const url = event.notification.data.link
event.waitUntil(
clients.matchAll({type: 'window'}).then( windowClients => {
// Check if there is already a window/tab open with the target URL
for (var i = 0; i < windowClients.length; i++) {
var client = windowClients[i];
// If so, just focus it.
if (client.url === url && 'focus' in client) {
return client.focus();
}
}
// If not, then open the target URL in a new window/tab.
if (clients.openWindow) {
return clients.openWindow(url);
}
})
);
})
messaging.onBackgroundMessage(async function(payload) {
console.log("Received background message ", payload)
const notificationTitle = payload.notification.title
const notificationOptions = {
body: payload.notification.body,
icon: './logo192.png',
badge: './notification-badgex24.png',
data: {
link: payload.data?.link
}
}
self.registration.showNotification(notificationTitle, notificationOptions)
})
service-worker.js
import { clientsClaim } from 'workbox-core';
import { ExpirationPlugin } from 'workbox-expiration';
import { precacheAndRoute, createHandlerBoundToURL } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import { StaleWhileRevalidate } from 'workbox-strategies';
clientsClaim();
const fileExtensionRegexp = new RegExp('/[^/?]+\\.[^/]+$');
registerRoute(
// Return false to exempt requests from being fulfilled by index.html.
({ request, url }) => {
// If this isn't a navigation, skip.
if (request.mode !== 'navigate') {
return false;
} // If this is a URL that starts with /_, skip.
if (url.pathname.startsWith('/_')) {
return false;
} // If this looks like a URL for a resource, because it contains // a file extension, skip.
if (url.pathname.match(fileExtensionRegexp)) {
return false;
} // Return true to signal that we want to use the handler.
return true;
},
createHandlerBoundToURL(process.env.PUBLIC_URL + '/index.html')
);
registerRoute(
// Add in any other file extensions or routing criteria as needed.
({ url }) => url.origin === self.location.origin && url.pathname.endsWith('.png'), // Customize this strategy as needed, e.g., by changing to CacheFirst.
new StaleWhileRevalidate({
cacheName: 'images',
plugins: [
// Ensure that once this runtime cache reaches a maximum size the
// least-recently used images are removed.
new ExpirationPlugin({ maxEntries: 50 }),
],
})
);
self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
});
Ok, after spending weeks, I figured it out. So for anyone else, using the service worker created using the default create react app, follow the steps below.
first create firebase-messaging-sw.js in public folder and put the same code as in the question, but this time also add importScripts("/service-worker.js").
// Scripts for firebase and firebase messaging
importScripts("https://www.gstatic.com/firebasejs/8.2.0/firebase-app.js")
importScripts("https://www.gstatic.com/firebasejs/8.2.0/firebase-messaging.js")
importScripts(`/service-worker.js`) // <- here notice this
.
.
.
The import will import the service-worker.js after the build step. The service worker you have in the src folder is only a template. You cannot use importScript in the service-worker.js file that's in the src folder as that will throw importScripts is not defined error.
Your build folder after your build step:
└── build/
├── static/
│ ├── .
│ ├── .
│ └── .
├── index.html
├── firebase-messaging-sw.js
├── service-worker.js
├── .
└── .
Now in index.html add
<script>
if ('serviceWorker' in navigator){
navigator.serviceWorker.register('/firebase-messaging-sw.js')
.then(reg => console.debug("Service worker registered sucessfully"))
}
</script>
And that's it. Both firebase-messaging and your PWA service worker will work
or you can also
You can create a new file called sw.js in your public folder and use imporScript() to import both firebase-messaging-sw.js and default service-worker.js
make sure to pass in service worker registration in getToken of the firebase
const registration = await navigator.serviceWorker.register('/sw.js')
const currentToken = await getToken(messaging, {
serviceWorkerRegistration: registration,
vapidKey: '<VAPID_KEY>'
})
now simply register your new service worker in index file as shown
I think one way of doing this is just having only firebase-messaging-sw.js file and using workbox to inject your service-worker.js into it
https://developer.chrome.com/docs/workbox/precaching-with-workbox/
forexample :
// build-sw.js
import {injectManifest} from 'workbox-build';
injectManifest({
swSrc: './src/sw.js',
swDest: './dist/firebase-messaging-sw.js',
globDirectory: './dist',
globPatterns: [
'**/*.js',
'**/*.css',
'**/*.svg'
]
});
and all firebase config must be in sw.js to be written in firebase-messaging-sw.js
and in your package.json simply run build-sw.js before react-script start
"scripts": {
"start": "node build-sw.js && react-scripts start",
}
or you can use react-app-rewired and workbox box plugin instead
We are currently hosting a react app with amplify and have recently enabled the service worker via workbox to become PWA compatible. Our amplify app uses Cloudflare + S3 to serve our built react app. After enabling the service worker, when pushing new code and triggering a build, we noticed that if a user was on the site and refreshed their app, it would get a 404 error
https://ourwebsite.com/static/js/main.3j3k232.chunk.js 404 not found
This error would persist until the user would do a hard refresh on the browser. Our service worker previously was set to createHandlerBoundToURL() but we opted for NetworkFirst() instead because we were curious about solving the N+1 update problem. This was after the createHandlerBoundToURL() version of the service worker was deployed already.
import { clientsClaim } from 'workbox-core';
import { ExpirationPlugin } from 'workbox-expiration';
import { precacheAndRoute, createHandlerBoundToURL } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import { NetworkFirst, NetworkOnly, StaleWhileRevalidate } from 'workbox-strategies';
declare const self: ServiceWorkerGlobalScope;
clientsClaim();
// Precache all of the assets generated by your build process.
// Their URLs are injected into the manifest variable below.
// This variable must be present somewhere in your service worker file,
// even if you decide not to use precaching. See https://cra.link/PWA
precacheAndRoute(self.__WB_MANIFEST);
// Set up App Shell-style routing, so that all navigation requests
// are fulfilled with your index.html shell. Learn more at
// https://developers.google.com/web/fundamentals/architecture/app-shell
const fileExtensionRegexp = new RegExp('/[^/?]+\\.[^/]+$');
registerRoute(
// Return false to exempt requests from being fulfilled by index.html.
({ request, url }: { request: Request; url: URL }) => {
// If this isn't a navigation, skip.
if (request.mode !== 'navigate') {
return false;
}
// If this is a URL that starts with /_, skip.
if (url.pathname.startsWith('/_')) {
return false;
}
// If this looks like a URL for a resource, because it contains
// a file extension, skip.
if (url.pathname.match(fileExtensionRegexp)) {
return false;
}
// Return true to signal that we want to use the handler.
return true;
},
// createHandlerBoundToURL(process.env.PUBLIC_URL + '/index.html') // initially had this enabled
new NetworkFirst()
);
// An example runtime caching route for requests that aren't handled by the
// precache, in this case same-origin .png requests like those from in public/
registerRoute(
// Add in any other file extensions or routing criteria as needed.
({ url }) =>
url.origin === self.location.origin && url.pathname.endsWith('.png'),
// Customize this strategy as needed, e.g., by changing to CacheFirst.
new StaleWhileRevalidate({
cacheName: 'images',
plugins: [
// Ensure that once this runtime cache reaches a maximum size the
// least-recently used images are removed.
new ExpirationPlugin({ maxEntries: 50 }),
],
})
);
// This allows the web app to trigger skipWaiting via
// registration.waiting.postMessage({type: 'SKIP_WAITING'})
self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
});
To reproduce this, we would only have to push new code and trigger a build, after it was completed the user would just need to refresh and they could be met with a 404 blank screen. We are using this PWA in a TWA android application.
Any help would be appreciated! We suspect it could also be an issue with CloudFront CDN as we have read that CDNs can cause issues with service workers and their caches.
I created a new project using CRA's npx create-react-app my-app --template cra-template-pwa command. When I build and test, the service worker worked, but index.html is also cached, so the change is not applied. I am wondering how can I prevent the precahce of index.html.
Is there a way to prevent precache in service-worker.js or serviceWorkerRegistration.js created by CRA? I tried to build using workbox-cli, but it fails because self.__WB_MANIFEST does not exist.
service-worker.js
/* eslint-disable no-restricted-globals */
// This service worker can be customized!
// See https://developers.google.com/web/tools/workbox/modules
// for the list of available Workbox modules, or add any other
// code you'd like.
// You can also remove this file if you'd prefer not to use a
// service worker, and the Workbox build step will be skipped.
import { clientsClaim } from "workbox-core";
import { ExpirationPlugin } from "workbox-expiration";
import { precacheAndRoute, createHandlerBoundToURL } from "workbox-precaching";
import { registerRoute } from "workbox-routing";
import { StaleWhileRevalidate } from "workbox-strategies";
clientsClaim();
// Precache all of the assets generated by your build process.
// Their URLs are injected into the manifest variable below.
// This variable must be present somewhere in your service worker file,
// even if you decide not to use precaching. See https://cra.link/PWA
precacheAndRoute(self.__WB_MANIFEST);
// Set up App Shell-style routing, so that all navigation requests
// are fulfilled with your index.html shell. Learn more at
// https://developers.google.com/web/fundamentals/architecture/app-shell
const fileExtensionRegexp = new RegExp("/[^/?]+\\.[^/]+$");
registerRoute(
// Return false to exempt requests from being fulfilled by index.html.
({ request, url }) => {
console.log(url);
// If this isn't a navigation, skip.
if (request.mode !== "navigate") {
return false;
} // If this is a URL that starts with /_, skip.
if (url.pathname.startsWith("/_")) {
return false;
} // If this looks like a URL for a resource, because it contains // a file extension, skip.
if (url.pathname.match(fileExtensionRegexp)) {
return false;
} // Return true to signal that we want to use the handler.
if (url.pathname.endsWith("/index.html")) {
return false;
}
return true;
},
createHandlerBoundToURL(process.env.PUBLIC_URL + "/index.html")
);
// An example runtime caching route for requests that aren't handled by the
// precache, in this case same-origin .png requests like those from in public/
registerRoute(
// Add in any other file extensions or routing criteria as needed.
({ url }) =>
url.origin === self.location.origin && url.pathname.endsWith(".png"), // Customize this strategy as needed, e.g., by changing to CacheFirst.
new StaleWhileRevalidate({
cacheName: "images",
plugins: [
// Ensure that once this runtime cache reaches a maximum size the
// least-recently used images are removed.
new ExpirationPlugin({ maxEntries: 50 }),
],
})
);
registerRoute(
({ url }) =>
url.origin === self.location.origin && url.pathname.endsWith(".html"),
new StaleWhileRevalidate({
cacheName: "html",
plugins: [new ExpirationPlugin({ maxEntries: 0 })],
})
);
// This allows the web app to trigger skipWaiting via
// registration.waiting.postMessage({type: 'SKIP_WAITING'})
self.addEventListener("message", (event) => {
if (event.data && event.data.type === "SKIP_WAITING") {
self.skipWaiting();
}
});
// Any other custom service worker logic can go here.
self.addEventListener("push", function (e) {
const data = e.data.json();
console.log(data);
self.registration.showNotification(data.title, {
body: data.message,
requireInteraction: true,
icon: "/favicon.ico",
badge: "/favicon.ico",
});
});
I am trying to run cypress test cases headless using cmd command
npx cypress run
But it gives me below error -
Do I need to install any dependency for this to load.
Even css files are not getting loaded.
Note : I haven't installed webpack or any other dependency. Only cypress is installed additionally.
Yes, you will need to extend the webpack configuration used by cypress to handle the files you would like to load. You can find an example here
Below I've modified the example to work with cypress 10.
// cypress.config.ts
import { defineConfig } from 'cypress';
import findWebpack from 'find-webpack';
import webpackPreprocessor from '#cypress/webpack-preprocessor';
const webpackOptions = findWebpack.getWebpackOptions();
const options = {
webpackOptions,
watchOptions: {},
};
export default defineConfig({
e2e: {
setupNodeEvents(on) {
// implement node event listeners here
// on('file:preprocessor', webpack(options));
// use a module that carefully removes only plugins
// that we found to be breaking the bundling
// https://github.com/bahmutov/find-webpack
const cleanOptions = {
reactScripts: true,
};
findWebpack.cleanForCypress(cleanOptions, webpackOptions);
on('file:preprocessor', webpackPreprocessor(options));
},
specPattern: 'src/**/*.cy.{js,jsx,ts,tsx}',
},
});
I'm building a React app using create-react-app and I used the following command to make it from the beginning a PWA.
npx create-react-app my-app --template cra-template-pwa
After building my app, going to the browser developer tools, and look into the cache, I noticed all my images and files are cached but not my manifest.json. I can see my manifest.json being in the app since I can look at it in the dev tools and I can install the app on my computer. The problem comes when I go offline, the manifest.json is not found because it's not cached.
How can I cache my manifest.json?
Why my manifest.json isn't cached as default?
This is the error I see in my console when I serve the app offline:
GET http://localhost:5000/manifest.json net::ERR_INTERNET_DISCONNECTED
200
Service Worker generated by CRA;
import { clientsClaim } from 'workbox-core';
import { ExpirationPlugin } from 'workbox-expiration';
import { precacheAndRoute, createHandlerBoundToURL } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import { StaleWhileRevalidate } from 'workbox-strategies';
clientsClaim();
// Precache all of the assets generated by your build process.
// Their URLs are injected into the manifest variable below.
// This variable must be present somewhere in your service worker file,
// even if you decide not to use precaching. See https://cra.link/PWA
precacheAndRoute(self.__WB_MANIFEST);
// Set up App Shell-style routing, so that all navigation requests
// are fulfilled with your index.html shell. Learn more at
// https://developers.google.com/web/fundamentals/architecture/app-shell
const fileExtensionRegexp = new RegExp('/[^/?]+\\.[^/]+$');
registerRoute(
// Return false to exempt requests from being fulfilled by index.html.
({ request, url }) => {
// If this isn't a navigation, skip.
if (request.mode !== 'navigate') {
return false;
} // If this is a URL that starts with /_, skip.
if (url.pathname.startsWith('/_')) {
return false;
} // If this looks like a URL for a resource, because it contains // a file extension, skip.
if (url.pathname.match(fileExtensionRegexp)) {
return false;
} // Return true to signal that we want to use the handler.
return true;
},
createHandlerBoundToURL(process.env.PUBLIC_URL + '/index.html')
);
// An example runtime caching route for requests that aren't handled by the
// precache, in this case same-origin .png requests like those from in public/
registerRoute(
// Add in any other file extensions or routing criteria as needed.
({ url }) => url.origin === self.location.origin && url.pathname.endsWith('.png'), // Customize this strategy as needed, e.g., by changing to CacheFirst.
new StaleWhileRevalidate({
cacheName: 'images',
plugins: [
// Ensure that once this runtime cache reaches a maximum size the
// least-recently used images are removed.
new ExpirationPlugin({ maxEntries: 50 }),
],
})
);
// This allows the web app to trigger skipWaiting via
// registration.waiting.postMessage({type: 'SKIP_WAITING'})
self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
});```