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

Switch to the Workbox InjectManifest plugin #9205

Merged
merged 26 commits into from
Jul 22, 2020
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
54d3740
WIP
jeffposnick Jun 11, 2020
e9fa48b
WIP
jeffposnick Jun 11, 2020
5a3a6c7
Rename
jeffposnick Jun 11, 2020
0eed437
Use publicPath
jeffposnick Jun 11, 2020
3308fb6
Getting closer.
jeffposnick Jun 11, 2020
b0cab1b
Move off of NavigationRoute
jeffposnick Jun 23, 2020
53b142a
Update the PWA guide
jeffposnick Jun 23, 2020
8707cc7
Don't precache any LICENSEs.
jeffposnick Jun 23, 2020
d2545aa
Updated the ignore instructions
jeffposnick Jun 23, 2020
3ee8b1f
skipWaiting message handler
jeffposnick Jun 23, 2020
a893129
Added a comment
jeffposnick Jun 23, 2020
8ff1fe3
Merge branch 'master' into wb-inject-manifest
ianschmitz Jun 26, 2020
f96d1d2
Add back web vitals to typescript template
ianschmitz Jun 26, 2020
c674f01
Add web vitals back to javascript template
ianschmitz Jun 26, 2020
d22c7dc
Change references of CRA 3 to 4
ianschmitz Jun 26, 2020
563589c
Add webworker lib ref
ianschmitz Jun 26, 2020
49b27d5
Fix TypeScript errors in service-worker.ts
ianschmitz Jun 26, 2020
f8245c9
Add back eslint disable comment
ianschmitz Jun 26, 2020
f3adc73
Ignore restricted-globals across entire file
ianschmitz Jun 26, 2020
94d6494
Runtime caching
jeffposnick Jun 26, 2020
d949faa
Explicitly depend on Workbox libs
jeffposnick Jun 29, 2020
b02ebb4
Remove SW and package.json deps
jeffposnick Jul 13, 2020
7f768c9
WIP update to README.
jeffposnick Jul 13, 2020
34a46dc
Updates to the docs.
jeffposnick Jul 13, 2020
419a883
Explicitly require InjectManifest
jeffposnick Jul 13, 2020
991e683
Switch back.
jeffposnick Jul 13, 2020
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
46 changes: 41 additions & 5 deletions docusaurus/docs/making-a-progressive-web-app.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,15 @@ title: Making a Progressive Web App
The production build has all the tools necessary to generate a first-class
[Progressive Web App](https://developers.google.com/web/progressive-web-apps/),
but **the offline/cache-first behavior is opt-in only**. By default,
the build process will generate a service worker file, but it will not be
the build process will compile a service worker file, but it will not be
registered, so it will not take control of your production web app.

If you know that you won't be using service workers, you can speed up the build
process by removing the
[`src/service-worker.js`](https://github.com/facebook/create-react-app/blob/master/packages/cra-template/template/src/service-worker.js)
file from your local project. This will skip the step that compiles your
service worker at build time.

In order to opt-in to the offline-first behavior, developers should look for the
following in their [`src/index.js`](https://github.com/facebook/create-react-app/blob/master/packages/cra-template/template/src/index.js) file:

Expand All @@ -34,14 +40,43 @@ However, they [can make debugging deployments more challenging](https://github.c

The [`workbox-webpack-plugin`](https://developers.google.com/web/tools/workbox/modules/workbox-webpack-plugin)
is integrated into production configuration,
and it will take care of generating a service worker file that will automatically
precache all of your local assets and keep them up to date as you deploy updates.
and it will take care of compiling a service worker file that will automatically
precache all of your `webpack`-generated assets and keep them up to date as you
deploy updates.
The service worker will use a [cache-first strategy](https://developers.google.com/web/fundamentals/instant-and-offline/offline-cookbook/#cache-falling-back-to-network)
for handling all requests for local assets, including
for handling all requests for `webpack`-generated assets, including
[navigation requests](https://developers.google.com/web/fundamentals/primers/service-workers/high-performance-loading#first_what_are_navigation_requests)
for your HTML, ensuring that your web app is consistently fast, even on a slow
or unreliable network.

Note: Resources that are not generated by `webpack`, such as static files that are
copied over from your local
[`public/` directory](https://github.com/facebook/create-react-app/blob/master/packages/cra-template/template/public/)
or third-party resources, will not be precached. You can optionally set up Workbox
[routes](https://developers.google.com/web/tools/workbox/guides/route-requests)
to apply the runtime caching strategy of your choice to those resources.

## Customization

Starting with Create React App 4, you have full control over customizing the
logic in this service worker, by making changes to
[`src/service-worker.js`](https://github.com/facebook/create-react-app/blob/master/packages/cra-template/template/src/service-worker.js).
You can use
[additional modules](https://developers.google.com/web/tools/workbox/modules)
from the Workbox project, add in a push notification library, or remove some of
the default caching logic. The one requirement is that you keep
`self.__WB_MANIFEST` somewhere in your file, as the Workbox compilation plugin
checks for this value when generating a manifest of URLs to precache. If you
would prefer not to use precaching, you can just assign `self.__WB_MANIFEST`
to a variable that will be ignored, like:

```js
// eslint-disable-next-line no-restricted-globals
const ignored = self.__WB_MANIFEST;

// Your custom service worker code goes here.
```

## Offline-First Considerations

If you do decide to opt-in to service worker registration, please take the
Expand Down Expand Up @@ -88,7 +123,8 @@ following into account:

1. By default, the generated service worker file will not intercept or cache any
cross-origin traffic, like HTTP [API requests](integrating-with-an-api-backend.md),
images, or embeds loaded from a different domain.
images, or embeds loaded from a different domain. Starting with Create
React App 4, this can be customized, as explained above.

## Progressive Web App Metadata

Expand Down
4 changes: 2 additions & 2 deletions packages/cra-template-typescript/template/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import * as serviceWorkerRegistration from './serviceWorkerRegistration';
ianschmitz marked this conversation as resolved.
Show resolved Hide resolved
import reportWebVitals from './reportWebVitals';

ReactDOM.render(
Expand All @@ -15,7 +15,7 @@ ReactDOM.render(
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://cra.link/PWA
serviceWorker.unregister();
serviceWorkerRegistration.unregister();

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
Expand Down
80 changes: 80 additions & 0 deletions packages/cra-template-typescript/template/src/service-worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/// <reference lib="webworker" />
/* 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';

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')
);

// 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();
}
});

// Any other custom service worker logic can go here.
4 changes: 2 additions & 2 deletions packages/cra-template/template/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import * as serviceWorkerRegistration from './serviceWorkerRegistration';
import reportWebVitals from './reportWebVitals';

ReactDOM.render(
Expand All @@ -15,7 +15,7 @@ ReactDOM.render(
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://cra.link/PWA
serviceWorker.unregister();
serviceWorkerRegistration.unregister();

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
Expand Down
78 changes: 78 additions & 0 deletions packages/cra-template/template/src/service-worker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/* 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 }) => {
// 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();
}
});

// Any other custom service worker logic can go here.
3 changes: 3 additions & 0 deletions packages/react-scripts/config/paths.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ module.exports = {
testsSetup: resolveModule(resolveApp, 'src/setupTests'),
proxySetup: resolveApp('src/setupProxy.js'),
appNodeModules: resolveApp('node_modules'),
swSrc: resolveModule(resolveApp, 'src/service-worker'),
publicUrlOrPath,
};

Expand All @@ -94,6 +95,7 @@ module.exports = {
testsSetup: resolveModule(resolveApp, 'src/setupTests'),
proxySetup: resolveApp('src/setupProxy.js'),
appNodeModules: resolveApp('node_modules'),
swSrc: resolveModule(resolveApp, 'src/service-worker'),
publicUrlOrPath,
// These properties only exist before ejecting:
ownPath: resolveOwn('.'),
Expand Down Expand Up @@ -129,6 +131,7 @@ if (
testsSetup: resolveModule(resolveOwn, `${templatePath}/src/setupTests`),
proxySetup: resolveOwn(`${templatePath}/src/setupProxy.js`),
appNodeModules: resolveOwn('node_modules'),
swSrc: resolveModule(resolveOwn, `${templatePath}/src/service-worker`),
publicUrlOrPath,
// These properties only exist before ejecting:
ownPath: resolveOwn('.'),
Expand Down
22 changes: 8 additions & 14 deletions packages/react-scripts/config/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ const imageInlineSizeLimit = parseInt(
// Check if TypeScript is setup
const useTypeScript = fs.existsSync(paths.appTsConfig);

// Get the path to the uncompiled service worker (if it exists).
const swSrc = paths.swSrc;

// style files regexes
const cssRegex = /\.css$/;
const cssModuleRegex = /\.module\.css$/;
Expand Down Expand Up @@ -692,20 +695,11 @@ module.exports = function (webpackEnv) {
// Generate a service worker script that will precache, and keep up to date,
// the HTML & assets that are part of the webpack build.
isEnvProduction &&
new WorkboxWebpackPlugin.GenerateSW({
clientsClaim: true,
exclude: [/\.map$/, /asset-manifest\.json$/],
importWorkboxFrom: 'cdn',
navigateFallback: paths.publicUrlOrPath + 'index.html',
navigateFallbackBlacklist: [
// Exclude URLs starting with /_, as they're likely an API call
new RegExp('^/_'),
// Exclude any URLs whose last part seems to be a file extension
// as they're likely a resource and not a SPA route.
// URLs containing a "?" character won't be blacklisted as they're likely
// a route with query params (e.g. auth callbacks).
new RegExp('/[^/?]+\\.[^/]+$'),
],
fs.existsSync(swSrc) &&
ianschmitz marked this conversation as resolved.
Show resolved Hide resolved
new WorkboxWebpackPlugin.InjectManifest({
swSrc,
dontCacheBustURLsMatching: /\.[0-9a-f]{8}\./,
ianschmitz marked this conversation as resolved.
Show resolved Hide resolved
exclude: [/\.map$/, /asset-manifest\.json$/, /LICENSE/],
}),
// TypeScript type checking
useTypeScript &&
Expand Down
14 changes: 13 additions & 1 deletion packages/react-scripts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,19 @@
"webpack": "4.43.0",
"webpack-dev-server": "3.11.0",
"webpack-manifest-plugin": "2.2.0",
"workbox-webpack-plugin": "4.3.1"
"workbox-background-sync": "^5.1.3",
"workbox-broadcast-update": "^5.1.3",
"workbox-cacheable-response": "^5.1.3",
"workbox-core": "^5.1.3",
"workbox-expiration": "^5.1.3",
"workbox-google-analytics": "^5.1.3",
"workbox-navigation-preload": "^5.1.3",
"workbox-precaching": "^5.1.3",
"workbox-range-requests": "^5.1.3",
"workbox-routing": "^5.1.3",
"workbox-strategies": "^5.1.3",
"workbox-streams": "^5.1.3",
"workbox-webpack-plugin": "5.1.3"
jeffposnick marked this conversation as resolved.
Show resolved Hide resolved
},
"devDependencies": {
"react": "^16.12.0",
Expand Down