Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proof of Concept: Add cache busting and auto-reloading to service workers #2563

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion packages/react-scripts/config/env.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ process.env.NODE_PATH = (process.env.NODE_PATH || '')
// injected into the application via DefinePlugin in Webpack configuration.
const REACT_APP = /^REACT_APP_/i;

function getClientEnvironment(publicUrl) {
function getClientEnvironment(publicUrl, serviceWorkerName) {
const raw = Object.keys(process.env)
.filter(key => REACT_APP.test(key))
.reduce(
Expand All @@ -84,6 +84,9 @@ function getClientEnvironment(publicUrl) {
// This should only be used as an escape hatch. Normally you would put
// images into the `src` and `import` them in code to get their paths.
PUBLIC_URL: publicUrl,
// package.json name property.
// Used by service worker to know the name of the generated service worker.
SERVICE_WORKER_NAME: serviceWorkerName,
}
);
// Stringify all values so we can feed into Webpack DefinePlugin
Expand Down
9 changes: 7 additions & 2 deletions packages/react-scripts/config/webpack.config.prod.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,13 @@ const shouldUseRelativeAssetPaths = publicPath === './';
// as %PUBLIC_URL% in `index.html` and `process.env.PUBLIC_URL` in JavaScript.
// Omit trailing slash as %PUBLIC_URL%/xyz looks better than %PUBLIC_URL%xyz.
const publicUrl = publicPath.slice(0, -1);
// generate service worker name
const serviceWorkerName = `service-worker-${Math.random()
.toString(16)
.substring(7)}.js`;

// Get environment variables to inject into our app.
const env = getClientEnvironment(publicUrl);
const env = getClientEnvironment(publicUrl, serviceWorkerName);

// Assert this just to be safe.
// Development builds of React are slow and not intended for production.
Expand Down Expand Up @@ -311,7 +316,7 @@ module.exports = {
// If a URL is already hashed by Webpack, then there is no concern
// about it being stale, and the cache-busting can be skipped.
dontCacheBustUrlsMatching: /\.\w{8}\./,
filename: 'service-worker.js',
filename: serviceWorkerName,
logger(message) {
if (message.indexOf('Total precache size is') === 0) {
// This message occurs for every build and is a bit too noisy.
Expand Down
83 changes: 58 additions & 25 deletions packages/react-scripts/template/src/registerServiceWorker.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,38 +10,71 @@

export default function register() {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// Hide document while we decide if this is the correct service worker
const bodyStyle = document.body.style.display;
document.body.style.display = 'none';

window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the old content will have been purged and
// the fresh content will have been added to the cache.
// It's the perfect time to display a "New content is
// available; please refresh." message in your web app.
console.log('New content is available; please refresh.');
} 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.');
}
}
};
};
const swUrl = `${process.env.PUBLIC_URL}/${process.env.SERVICE_WORKER_NAME}`;

// This is used to check validaity of service worker. And reload page if changed had been made.
fetch(swUrl)
.then(response => {
// Ensure service worker exists, and that we really are getting a JS file.
if (
response.status === 404 ||
response.headers.get('content-type').indexOf('javascript') === -1
) {
navigator.serviceWorker.ready.then(registration => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
// Found original service worker.
document.body.style.display = bodyStyle;
registerValidSW(swUrl);
}
})
.catch(error => {
console.error('Error during service worker registration:', error);
.catch(() => {
document.body.style.display = bodyStyle;
console.log(
'No internet connection found. App is running in offline mode.'
);
});
});
}
}

function registerValidSW(swUrl) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the old content will have been purged and
// the fresh content will have been added to the cache.
// It's the perfect time to display a "New content is
// available; please refresh." message in your web app.
console.log('New content is available; please refresh.');
} 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.');
}
}
};
};
})
.catch(error => {
console.error('Error during service worker registration:', error);
});
}

export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then(registration => {
Expand Down