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

PWA-ification, via SWPrecacheWebpackPlugin + manifest.json #1728

Merged
merged 25 commits into from
May 17, 2017
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
8289386
sw-precache-webpack-plugin, SW registration, manifest.json
jeffposnick Feb 11, 2017
75088ab
Documentation + a few SW tweaks.
jeffposnick Feb 12, 2017
bfed215
Added an unregister method, too, just in case.
jeffposnick Feb 16, 2017
2835105
More info for the READMEs.
jeffposnick Feb 16, 2017
2dc418b
Add minify to SWPrecacheWebpackPlugin config
goldhand Feb 17, 2017
21d14bb
Fix SWPrecacheWebpackPlugin typo
goldhand Feb 17, 2017
a681e91
Fix file references in READMEmd
ianschmitz Feb 17, 2017
a9fb906
Add instructions for testing service-worker locally
goldhand Feb 17, 2017
1cdb4cd
Merge pull request #2 from goldhand/pwa
jeffposnick Mar 3, 2017
4e3ede4
Review feedback
jeffposnick Mar 3, 2017
86d590d
Some additional PWA metadata
jeffposnick Mar 5, 2017
0640a20
Merge remote-tracking branch 'upstream/master' into pwa
jeffposnick Mar 5, 2017
cb40bd7
Use sw-precache-webpack-plugin v0.9.1 for node >=4.0.0 support
goldhand Mar 10, 2017
6aa8fe8
Merge pull request #4 from goldhand/pwa
jeffposnick Mar 27, 2017
444f0ca
Merge remote-tracking branch 'upstream/master' into pwa
jeffposnick Apr 27, 2017
6a880f8
Review feedback.
jeffposnick May 9, 2017
5ea92ae
Add manifest.json context in a comment.
jeffposnick May 9, 2017
f1b0f1d
Merge remote-tracking branch 'upstream/master' into pwa
jeffposnick May 15, 2017
074fc7e
Merge branch 'master' into pwa
gaearon May 16, 2017
804e858
Fix typo
gaearon May 16, 2017
f9b303a
Merge branch 'master' into pwa
Timer May 17, 2017
502cbdf
Downgrade to sw-precache-webpack-plugin 0.9.1
jeffposnick May 17, 2017
9ea16b7
Hide changes in README until 1.0
gaearon May 17, 2017
f8f3e20
Hide changes in User Guide until 1.0
gaearon May 17, 2017
27424ce
Hide more docs
gaearon May 17, 2017
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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,8 @@ Builds the app for production to the `build` folder.<br>
It correctly bundles React in production mode and optimizes the build for the best performance.

The build is minified and the filenames include the hashes.<br>
Your app is ready to be deployed!
A [service worker](https://developers.google.com/web/fundamentals/getting-started/primers/service-workers) using an [offline-first caching strategy](https://developers.google.com/web/fundamentals/instant-and-offline/offline-cookbook/#cache-falling-back-to-network) is automatically generated.<br>
Your ([progressive web](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md#making-a-progressive-web-app)) app is ready to be deployed!

## User Guide

Expand Down Expand Up @@ -164,6 +165,7 @@ Please refer to the [User Guide](https://github.com/facebookincubator/create-rea
* Import CSS and image files directly from JavaScript.
* Autoprefixed CSS, so you don’t need `-webkit` or other prefixes.
* A `build` script to bundle JS, CSS, and images for production, with sourcemaps.
* An offline-first [service worker](https://developers.google.com/web/fundamentals/getting-started/primers/service-workers) and a [web app manifest](https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/), meeting all the [Progressive Web App](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md#making-a-progressive-web-app) criteria.

**The feature set is intentionally limited**. It doesn’t support advanced features such as server rendering or CSS modules. The tool is also **non-configurable** because it is hard to provide a cohesive experience and easy updates across a set of tools when the user can tweak anything.

Expand Down
8 changes: 8 additions & 0 deletions packages/react-scripts/config/webpack.config.prod.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const HtmlWebpackPlugin = require('html-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const ManifestPlugin = require('webpack-manifest-plugin');
const InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin');
const SWPrecacheWebpackPlugin = require('sw-precache-webpack-plugin');
const paths = require('./paths');
const getClientEnvironment = require('./env');

Expand Down Expand Up @@ -281,6 +282,13 @@ module.exports = {
new ManifestPlugin({
fileName: 'asset-manifest.json',
}),
new SWPrecacheWebpackPlugin({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A brief comment explaining the purpose of this plugin would be nice.

e.g.

// Generates the files required for a service worker

dontCacheBustUrlsMatching: /\.\w{8}\./,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't believe it's immediately apparent what this is doing, we should probably add a comment explaining we're trying to match any files with a hash (as generated by webpack).

This configuration is handed to the user when they eject, so anything to help them understand what's going on better. 😄

filename: 'service-worker.js',
minify: true,
navigateFallback: publicUrl + '/index.html',
staticFileGlobsIgnorePatterns: [/\.map$/, /asset-manifest\.json$/],
}),
],
// Some libraries import Node modules but don't use them in the browser.
// Tell Webpack to provide empty mocks for them so importing them works.
Expand Down
1 change: 1 addition & 0 deletions packages/react-scripts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"promise": "7.1.1",
"react-dev-utils": "^0.5.2",
"style-loader": "0.16.1",
"sw-precache-webpack-plugin": "^0.10.1",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please pin the plugin version.

"url-loader": "0.5.8",
"webpack": "2.4.1",
"webpack-dev-server": "2.4.4",
Expand Down
94 changes: 93 additions & 1 deletion packages/react-scripts/template/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1228,7 +1228,90 @@ Learn more about React Storybook:

## Making a Progressive Web App

You can turn your React app into a [Progressive Web App](https://developers.google.com/web/progressive-web-apps/) by following the steps in [this repository](https://github.com/jeffposnick/create-react-pwa).
By default, the production build is a fully functional, offline-first
[Progressive Web App](https://developers.google.com/web/progressive-web-apps/).

The [`sw-precache-webpack-plugin`](https://github.com/goldhand/sw-precache-webpack-plugin)
is integrated into [`webpack.config.prod.js`](../config/webpack.config.prod.js),
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.
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 the initial HTML, ensuring
that you web app is reliably fast, even on a slow or unreliable network.

It includes a [web app manifest](https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/),
located at [`public/manifest.json`](public/manifest.json), that you might want
to customize with metadata specific to your web application, such as its name
and branding colors.

If you would prefer not to enable service workers prior to your initial
production deployment, then remove the call to `serviceWorkerRegistration.register()`
from [`src/index.js`](src/index.js).

If you had previously enabled service workers in your production deployment and
have decided that you would like to disable them for all your existing users,
you can swap out the call to `serviceWorkerRegistration.register()` in
[`src/index.js`](src/index.js) with a call to `serviceWorkerRegistration.unregister()`.
After the user visits a page that has `serviceWorkerRegistration.unregister()`,
the service worker will be uninstalled.

### Offline-First Considerations

1. Service workers [require HTTPS](https://developers.google.com/web/fundamentals/getting-started/primers/service-workers#you_need_https),
although to facilitate local testing, that policy
[does not apply to `localhost`](http://stackoverflow.com/questions/34160509/options-for-testing-service-workers-via-http/34161385#34161385).
If your production web server does not support HTTPS, then the service worker
registration will fail, but the rest of your web app will remain functional.

1. Service workers are [not currently supported](https://jakearchibald.github.io/isserviceworkerready/)
in all web browsers. Service worker registration [won't be attempted](src/service-worker-registration.js)
on browsers that lack support.

1. The service worker is only enabled in the [production environment](#deployment),
e.g. the output of `npm run build`. It's recommended that you do not enable an
offline-first service worker in a development environment, as it can lead to
frustration when previously cached assets are used and do not include the latest
changes you've made locally.

1. If you *need* to test your offline-first service worker locally, build
the application (using `npm run build`) and run a simple http server from your
build directory. After running the build script, `create-react-app` will give
instructions for one way to test your production build locally using
`pushstate-server` and the [deployment instructions](#deployment) have
instructions for using the python `SimpleHTTPServer`. *Be sure to always use an
incognito window to avoid complications with your browser cache.*

1. If possible,configure your production environment to serve the generated
`service-worker.js` [with HTTP caching disabled](http://stackoverflow.com/questions/38843970/service-worker-javascript-update-frequency-every-24-hours).
If that's not possible—[GitHub Pages](#github-pages), for instance, does not
allow you to change the default 10 minute HTTP cache lifetime—then be aware
that if you visit your production site, and then revisit again before
`service-worker.js` has expired from your HTTP cache, you'll continue to get
the previously cached assets from the service worker. If you have an immediate
need to view your updated production deployment, performing a shift-refresh
will temporarily disable the service worker and retrieve all assets from the
network.

1. Users aren't always familiar with offline-first web apps. It can be useful to
[let the user know](https://developers.google.com/web/fundamentals/instant-and-offline/offline-ux#inform_the_user_when_the_app_is_ready_for_offline_consumption)
when the service worker has finished populating your caches (showing a "This web
app works offline!" message) and also let them know when the service worker has
fetched the latest updates that will be available the next time they load the
page (showing a "New content is available; please refresh." message). Showing
this messages is currently left as an exercise to the developer, but as a
starting point, you can make use of the logic included in [`src/service-worker-registration.js`](src/service-worker-registration.js), which
demonstrates which service worker lifecycle events to listen for to detect each
scenario, and which as a default, just logs appropriate messages to the
JavaScript console.

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),
images, or embeds loaded from a different domain. If you would like to use a
runtime caching strategy for those requests, you can [`eject`](#npm-run-eject)
and then configure the
[`runtimeCaching`](https://github.com/GoogleChrome/sw-precache#runtimecaching-arrayobject)
option in the `SWPrecacheWebpackPlugin` section of
[`webpack.config.prod.js`](../config/webpack.config.prod.js).

## Deployment

Expand Down Expand Up @@ -1305,6 +1388,15 @@ It will get copied to the `build` folder when you run `npm run build`.

Now requests to `/todos/42` will be handled correctly both in development and in production.

On a production build, and in a browser that supports [service workers](https://developers.google.com/web/fundamentals/getting-started/primers/service-workers),
the service worker will automatically handle all navigation requests, like for
`/todos/42`, by serving the cached copy of your `index.html`. This
service worker navigation routing can be configured or disabled by
[`eject`ing](#npm-run-eject) and then modifying the
[`navigateFallback`](https://github.com/GoogleChrome/sw-precache#navigatefallback-string)
and [`navigateFallbackWhitelist`](https://github.com/GoogleChrome/sw-precache#navigatefallbackwhitelist-arrayregexp)
options of the `SWPreachePlugin` [configuration](../config/webpack.config.prod.js).

### Building for Relative Paths

By default, Create React App produces a build assuming your app is hosted at the server root.<br>
Expand Down
2 changes: 2 additions & 0 deletions packages/react-scripts/template/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
<!--
Notice the use of %PUBLIC_URL% in the tag above.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small nit, but maybe change this line to in the tags above, since there's multiple now. 😛

Expand Down
15 changes: 15 additions & 0 deletions packages/react-scripts/template/public/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"short_name": "React App",
"name": "React App",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should make the name more verbose than the short_name to press the point.
Also, I'd like to see some documentation explaining where and how these are used, and that Chrome favors short_name.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than detail everything in the create-react-app docs, I've added an explicit section for "Progressive Web App Metadata" that includes a brief overview and points developers to https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/

That link should remain authoritative.

"icons": [
{
"src": "favicon.ico",
"sizes": "192x192",
"type": "image/png"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably add documentation explaining to update these fields.
Are the type and sizes fields necessary? We want to avoid having to update multiple locations to get things working -- if we could automatically deduce this based on index.html that'd be even better (autogen this entire file at build instead of having it premade).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, my understanding is that type and sizes are required: https://developers.google.com/web/updates/2015/10/splashscreen

While there is a good amount of overlap between metadata that's in index.html and what's in this manifest.json file, there are things that you couldn't trivially deduce—like the background_color, the sizes of the icon, or the distinction between the short_name and name.

I'm a little hesitant to go down the route of requiring developers to partially populate manifest.json and then infer the rest of the fields from index.html, as that feels a bit too magic to me.

I'll definitely add some documentation explaining manifest.json in general, though.

}
],
"start_url": "./",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be ./index.html? Explicitness is always preferred where possible.

"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
3 changes: 3 additions & 0 deletions packages/react-scripts/template/src/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import registerServiceWorker from './service-worker-registration';
import './index.css';

ReactDOM.render(<App />, document.getElementById('root'));

registerServiceWorker();
38 changes: 38 additions & 0 deletions packages/react-scripts/template/src/service-worker-registration.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
export default function register() {
Copy link
Contributor

@Timer Timer May 7, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we put this in react-dev-utils and require (a) callback(s) to be passed for the results?
The less code we can burden the developer with the better.

If so, we need to make sure we run this code through babel so it's IE9+ compliant (minus module syntax).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apologies if I'm misunderstanding something about the project organization, but doesn't react-dev-utils contain a bunch of code that is consumed by node, and used optionally at build time? This particular code is consumed by the browser at runtime, and the hope is that most developers would end up using it.

There is the option of excluding registration, or explicitly switching to unregister() if needed (which is explained in the documentation), so putting this code in a place that's visible to the developer isn't necessarily a bad thing.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically react-dev-utils contains a mix of things that are useful either in Node or web context. Since every entry point is separate, it’s fine to add things that are browser-only there.

On the other hand, I don’t think we really want the generated app to depend on react-dev-utils explicitly, and doing so implicitly (relying on npm 3 flattening) is more confusing. So I’d leave it as is.

if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
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.');
}
}
};
};
}).catch(error => {
console.error('Error during service worker registration:', error);
});
});
}
}

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