Create-react-app: Guidlines for Using Workbox

Created on 1 Nov 2018  路  22Comments  路  Source: facebook/create-react-app

I am on React-Scripts 2.0.5. I am eager to use the new Workbox functionality that is mentioned in the release notes for 2.0. I can't find any directions on how to do it though.

There are posts on Medium, Freecodecamp and such that are really just hacks to the 1.x versions of React-scripts.

I had thought since it has just been added as a dependency we could get some assistance on how and where to add configurations. When I include Workbox in my registerServiceWorker.js and attempt to set some strategies on assets it crashes the entire app once built.

Unable to import module 'workbox-routing' from 'WORKBOX_CDN_ROOT_URL/workbox-routing.prod.js'.

I understand serviceworkers are now opt-in.
I understand that in the latest version of create react app the service worker register file is now just called serviceworker.js.

I compared the old registerServiceWorker.js to the new serviceworker.js and they are the same. I also am able to build with the default service worker file and I do see Workbox in the application tab in Chrome dev tools. I know its working out of the box. I just want to be able to edit it. Guidance would be greatly appreciated.

stale

Most helpful comment

Here's how I've done it with CRA and without ejecting. It uses the workbox build workflow. (cra 2.x)

TL;DR - This involves not using the SW generated by create-react-app but generating your own SW with injectManifest mode using the Workbox build workflow. Build commands will be added to react-build script in package.json

NOTE:
Keep an eye on #5369 I think it will allow configuring Workbox using the Webpack workflow using a custom configuration file.

Step1 : Make sure you register for a SW in the main index.js file. i.e. change serviceWorker.unregister(); to serviceWorker.register();

Step2: Inside src/serviceWorker.js in window.addEventListener('load', () => { change the swURL to point to a new file (this is what will be created in the build step)
so, change this - const swUrl = ``${process.env.PUBLIC_URL}/service-worker.js``;
to const swUrl = ``${process.env.PUBLIC_URL}/sw.js``;

Step3: Create two files under src/ - sw-build.js and sw.js

Step4: In sw-build.js add the code as provided by Google's injectManifest example here -

const workboxBuild = require('workbox-build');
// NOTE: This should be run *AFTER* all your assets are built
const buildSW = () => {
  // This will return a Promise
  return workboxBuild.injectManifest({
    swSrc: 'src/sw.js', // this is your sw template file
    swDest: 'build/sw.js', // this will be created in the build step
    globDirectory: 'build',
    globPatterns: [
      '**\/*.{js,css,html,png}',
    ]
  }).then(({count, size, warnings}) => {
    // Optionally, log any warnings and details.
    warnings.forEach(console.warn);
    console.log(`${count} files will be precached, totaling ${size} bytes.`);
  });
}
buildSW();

Step 5: Inside your custom service worker template file i.e. src/sw.js add the injection point and your cache rules. e.g - in the file src/sw.js -

if ('function' === typeof importScripts) {
  importScripts(
    'https://storage.googleapis.com/workbox-cdn/releases/3.5.0/workbox-sw.js'
  );
  /* global workbox */
  if (workbox) {
    console.log('Workbox is loaded');

    /* injection point for manifest files.  */
    workbox.precaching.precacheAndRoute([]);

/* custom cache rules*/
workbox.routing.registerNavigationRoute('/index.html', {
      blacklist: [/^\/_/, /\/[^\/]+\.[^\/]+$/],
    });

workbox.routing.registerRoute(
      /\.(?:png|gif|jpg|jpeg)$/,
      workbox.strategies.cacheFirst({
        cacheName: 'images',
        plugins: [
          new workbox.expiration.Plugin({
            maxEntries: 60,
            maxAgeSeconds: 30 * 24 * 60 * 60, // 30 Days
          }),
        ],
      })
    );

} else {
    console.log('Workbox could not be loaded. No Offline support');
  }
}

step 6: npm install workbox-build --save-dev
I think workbox-build is already included in node_modules, but good idea to make an entry in package.json

step 7: in package.json add the workbox build step
so, in package.json, under scripts add a new entry - "build-sw": "node ./src/sw-build.js"
and then change: "build": "react-scripts build
to "build": "react-scripts build && npm run build-sw

When your run npm run build you will see the the file sw.js inside your build folder. The CRA generated service-worker.js will continue to be there. You can run a build command to clear it, but it won't be used because we change the swURL in step2.

Again, all of this may not be relevant if #5369 provides a way to use the webpack flow by providing a custom configuration file.

All 22 comments

Im stuck in the same boat..

After struggeling with how the magic works i finally figured out what needs to be done to be able to "customize" the service worker routes:

First run npm run eject
Than go into /config/webpack.config.prod.js
Replace
new WorkboxWebpackPlugin.GenerateSW({ clientsClaim: true, exclude: [/\.map$/, /asset-manifest\.json$/], importWorkboxFrom: 'cdn', navigateFallback: publicUrl + '/index.html', navigateFallbackBlacklist: [ // Exclude URLs starting with /_, as they're likely an API call new RegExp('^/_'), // Exclude URLs containing a dot, as they're likely a resource in // public/ and not a SPA route new RegExp('/[^/]+\\.[^/]+$'), ], }),

with

new WorkboxWebpackPlugin.InjectManifest({ swSrc: './public/service-worker.js', }),

where as swSrc point to your custom service-worker.

My Service-Worker.js looks like this:
if (workbox) {
console.log(Yay! Workbox is loaded 馃帀);
} else {
console.log(Boo! Workbox didn't load 馃槵);
}
workbox.clientsClaim();
self.__precacheManifest = [].concat(self.__precacheManifest || []);
workbox.precaching.suppressWarnings();
workbox.precaching.precacheAndRoute(self.__precacheManifest, {});

workbox.routing.registerRoute(
'http://localhost:3000',
workbox.strategies.networkFirst()
)

workbox.routing.registerNavigationRoute("/index.html", {
blacklist: [/^/_/, //[^/]+.[^/]+$/],
});

As mentioned, @Rainson12 , there have already been a few articles about ejecting and doing something similar to what you have done. I believe the official way to customize using Workbox w/o ejecting is in the works. I personally don't want to eject. This issue here documents the progress of this new feature(I believe), https://github.com/facebook/create-react-app/pull/5369

@Nick-t-go from what i have seen in the other blog posts they all dont work with the current v2 create react app scripts as they all try to get rid of sw-precache and use workbox instead. Now with v2 you dont have to get rid of sw-precache anymore and because of that, all guides i have seen are obsolete and do not work anymore

Here's how I've done it with CRA and without ejecting. It uses the workbox build workflow. (cra 2.x)

TL;DR - This involves not using the SW generated by create-react-app but generating your own SW with injectManifest mode using the Workbox build workflow. Build commands will be added to react-build script in package.json

NOTE:
Keep an eye on #5369 I think it will allow configuring Workbox using the Webpack workflow using a custom configuration file.

Step1 : Make sure you register for a SW in the main index.js file. i.e. change serviceWorker.unregister(); to serviceWorker.register();

Step2: Inside src/serviceWorker.js in window.addEventListener('load', () => { change the swURL to point to a new file (this is what will be created in the build step)
so, change this - const swUrl = ``${process.env.PUBLIC_URL}/service-worker.js``;
to const swUrl = ``${process.env.PUBLIC_URL}/sw.js``;

Step3: Create two files under src/ - sw-build.js and sw.js

Step4: In sw-build.js add the code as provided by Google's injectManifest example here -

const workboxBuild = require('workbox-build');
// NOTE: This should be run *AFTER* all your assets are built
const buildSW = () => {
  // This will return a Promise
  return workboxBuild.injectManifest({
    swSrc: 'src/sw.js', // this is your sw template file
    swDest: 'build/sw.js', // this will be created in the build step
    globDirectory: 'build',
    globPatterns: [
      '**\/*.{js,css,html,png}',
    ]
  }).then(({count, size, warnings}) => {
    // Optionally, log any warnings and details.
    warnings.forEach(console.warn);
    console.log(`${count} files will be precached, totaling ${size} bytes.`);
  });
}
buildSW();

Step 5: Inside your custom service worker template file i.e. src/sw.js add the injection point and your cache rules. e.g - in the file src/sw.js -

if ('function' === typeof importScripts) {
  importScripts(
    'https://storage.googleapis.com/workbox-cdn/releases/3.5.0/workbox-sw.js'
  );
  /* global workbox */
  if (workbox) {
    console.log('Workbox is loaded');

    /* injection point for manifest files.  */
    workbox.precaching.precacheAndRoute([]);

/* custom cache rules*/
workbox.routing.registerNavigationRoute('/index.html', {
      blacklist: [/^\/_/, /\/[^\/]+\.[^\/]+$/],
    });

workbox.routing.registerRoute(
      /\.(?:png|gif|jpg|jpeg)$/,
      workbox.strategies.cacheFirst({
        cacheName: 'images',
        plugins: [
          new workbox.expiration.Plugin({
            maxEntries: 60,
            maxAgeSeconds: 30 * 24 * 60 * 60, // 30 Days
          }),
        ],
      })
    );

} else {
    console.log('Workbox could not be loaded. No Offline support');
  }
}

step 6: npm install workbox-build --save-dev
I think workbox-build is already included in node_modules, but good idea to make an entry in package.json

step 7: in package.json add the workbox build step
so, in package.json, under scripts add a new entry - "build-sw": "node ./src/sw-build.js"
and then change: "build": "react-scripts build
to "build": "react-scripts build && npm run build-sw

When your run npm run build you will see the the file sw.js inside your build folder. The CRA generated service-worker.js will continue to be there. You can run a build command to clear it, but it won't be used because we change the swURL in step2.

Again, all of this may not be relevant if #5369 provides a way to use the webpack flow by providing a custom configuration file.

What if:
1) We made our own custom copy of webpack.config.prod.js
Which is currently hosting the WorkboxWebpackPlugin and is setting the config at line 515:

new WorkboxWebpack.GenerateSW... .

2) Then in our build script include a script that replaces the default webpack.config.prod.js with our own _custom_ copy of webpack.config.prod.js
3) In our custom copy we can configure what we would like using the API set here.

Anyone have any ideas on whether or not this will work?

Not sure if it will work because, in an un-ejecting CRA, we don't have control over the webpack.cofig. #5369 will address the same problem. I think they will provide a workbox.config that will be read by the CRA's webpack.config to allow customizations ...

Also discussed here - #5359

@karannagupta finally circling back to this. going to give your suggestion a whirl. Is it possible for you to edit your post and fix the formatting. If it does work I think others will find is useful but more so if it is actually formatted properly to enhance readability.

@karannagupta Hey that worked! Thanks for sharing. Anyone who stumbles upon this looking for a way to edit the config for using Workbox with CRA without ejecting please check out the comment above, looks like the official fix is on the slow burn.

another way to get this to work is to use react-app-rewired, this override in config-overrides.js seems to be working:

const WorkboxWebpackPlugin = require('workbox-webpack-plugin');

module.exports = function override(config, env) {
  config.plugins = config.plugins.map(plugin => {
    if (plugin.constructor.name === 'GenerateSW') {
      return new WorkboxWebpackPlugin.InjectManifest({
        swSrc: './src/sw.js',
        swDest: 'service-worker.js',
      });
    }

    return plugin;
  });

  return config;
};

I generated a project with npx create-react-app my-app-ts --typescript,
then I created two files, sw-build.js and sw.js under src/,

There is an error:

Cannot compile namespaces when the '--isolatedModules' flag is provided.

image

What should I do?

I've excluded the SW files in tsconfig.json like this:

{
 "compilerOptions": {
  ...
  },
  "include": [
    "src/*"
  ],
  "exclude": [
    "node_modules",
    "**/*.spec.ts",
    "src/sw-*.js",
    "src/idb.js"
  ]
}

@karannagupta - could you make a gist or a repo describing your workaround please? It's getting a bit hard following it.

@karannagupta & others: i think #5369 would allow you to pass custom config to the GenerateSW plugin but wouldn鈥檛 allow you to use the InjectManifest plugin.

Has anyone else tried @karannagupta's method and run into problems debugging locally? When I deploy with the ootb service worker, it is hooked up and running no problem. When I use the example here, and validate that the correct files are ouput into my /build directory (I've run through the directions multiple times at this point), I start running into errors - specifically it looks like CRA may be looking for "service-worker.js" when it compiles and deploys. When my service worker is named sw.js, it does not register successfully. If I change the name of my generated sw.js file to service-worker.js, a service worker is registered but it is the "no-op" version that CRA inserts when debugging locally.

Hi @bscaspar, what errors do you run into? Or is it after you run debug some file?

I've attached screenshots of my console with Workbox messages for localhost ( using serve -s build ) that uses the custom sw.js for reference.

I also clear out the default CRA service-worker.js file like this -

    "build-sw": "node ./src/sw-build.js",
    "clean-cra-sw": "rm -f build/precache-manifest.*.js && rm -f build/service-worker.js",
    "build": "react-scripts build && npm run build-sw && npm run clean-cra-sw",

workbox_sw

workbox

Thanks for getting back to me @karannagupta turns out I was missing the serve step - previously I was testing by building then running the app with npm run start. Things are running smoothly with serve, thanks again for putting this workaround together!

This issue has been automatically marked as stale because it has not had any recent activity. It will be closed in 5 days if no further activity occurs.

This issue has been automatically closed because it has not had any recent activity. If you have a question or comment, please open a new issue.

Another addition to this thread (which was very instructive).
(Source and context on SO)

Append your code to generated service-worker

There's a module for it: cra-append-sw.
You're in charge to provide the appended code.

___Pros___: easy setup, takes advantage of GenerateSW

___Cons___: appended code is processed with Babel/Webpack, but not using CRA's config (you could opt-out). Still use GenerateSW which handle network caching for you. Not sure it works when developing locally

Use Workbox in custom service-worker file

  • in src/index.js enable service worker:
    js // serviceWorker.unregister() serviceWorker.register()
  • in src/serviceWorker.js register your custom file:

    // if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
    if ('serviceWorker' in navigator) {
    

    js // const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; const swUrl = `${process.env.PUBLIC_URL}/custom-service-worker.js`;
    You have to change the name cause when running dev server, CRA provides a mock for service-worker.js

  • in src/ folder, create custom-service-worker.js file. It will be processed by Webpack, so you can use ES2016/TypeScript syntax and import modules

    /* eslint no-restricted-globals: "off" */
    import * as precaching from 'workbox-precaching'
    // your own imports
    
    if (self.__precacheManifest) {
      precaching.precacheAndRoute(self.__precacheManifest)
    }
    
    // your own code
    
  • install react-app-rewire:

    • npm add --save-dev react-app-rewired
    • in package.json, in "scripts", replace react-scripts with react-app-rewired
  • tweak webpack configuration: create config-overrides.js in root folder:

    const WebpackBeforeBuildPlugin = require('before-build-webpack')
    const WorkboxWebpackPlugin = require('workbox-webpack-plugin')
    const path = require('path')
    const merge = require('lodash.merge')
    const fs = require('fs')
    
    // from https://www.viget.com/articles/run-multiple-webpack-configs-sequentially/
    class WaitPlugin extends WebpackBeforeBuildPlugin {
      constructor(file, interval = 100, timeout = 60e3) {
        super(function(stats, callback) {
          const start = Date.now()
    
          function poll() {
            if (fs.existsSync(file)) {
              callback()
            } else if (Date.now() - start > timeout) {
              throw Error(`Couldn't access ${file} within ${timeout}s`)
            } else {
              setTimeout(poll, interval)
            }
          }
          poll()
        })
      }
    }
    
    const swOutputName = 'custom-service-worker.js'
    const workerSource = path.resolve(__dirname, 'src', swOutputName)
    
    module.exports = {
      webpack: (config, env) => {
        // we need 2 webpack configurations:
        // 1- for the service worker file.
        //    it needs to be processed by webpack (to include 3rd party modules), and the output must be a
        //    plain, single file, not injected in the HTML page
        const swConfig = merge({}, config, {
          name: 'service worker',
          entry: workerSource,
          output: {
            filename: swOutputName
          },
          optimization: {
            splitChunks: false,
            runtimeChunk: false
          }
        })
        delete swConfig.plugins
    
        // 2- for the main application.
        //    we'll reuse configuration from create-react-app, without a specific Workbox configuration,
        //    so it could inject workbox-precache module and the computed manifest into the BUILT service-worker.js file.
        //    this require to WAIT for the first configuration to be finished
        if (env === 'production') {
          const builtWorkerPath = path.resolve(config.output.path, swOutputName)
          config.name = 'main-application'
          config.plugins.push(
            new WorkboxWebpackPlugin.InjectManifest({
              swSrc: builtWorkerPath,
              swDest: swOutputName
            }),
            new WaitPlugin(builtWorkerPath)
          )
        }
    
        // remove Workbox service-worker.js generator
        const removed = config.plugins.findIndex(
          ({ constructor: { name } }) => name === 'GenerateSW'
        )
        if (removed !== -1) {
          config.plugins.splice(removed, 1)
        } 
    
        const result = [swConfig, config]
        // compatibility hack for CRA's build script to support multiple configurations
        // https://github.com/facebook/create-react-app/blob/master/packages/react-scripts/scripts/build.js#L119
        result.output = { publicPath: config.output.publicPath }
        return result
      }
    }
    

___Pros___: you can use ES2016/TypeScript code in service-worker file. You still benefit from Workbox network caching facilities, with total control on it

___Cons___: complicated and fragile, because of the multiple configuration hack.

I've used the last solution, inspired from @karannagupta and @maciejsimka responses, cause I needed both caching code from Workbox and some import in my service worker file.

react-app-rewire-workbox may help simplifying the Webpack configuration (the one for main app). To be tested.

@karannagupta Could you elaborate on the difference between your solution (steps 1-7) above, https://github.com/facebook/create-react-app/issues/5673#issuecomment-438654051, and using Craco, https://github.com/sharegate/craco plus https://github.com/kevinsperrine/craco-workbox ? I'm just trying to understand this better. Thanks

Hi @ripern

I didn't look into great details of Craco, but in the end, it simply boils down to using Create React App without ejecting and being okay with no control over webpack's config. The difference in my approach is that it uses the Workbox build workflow, but if CRA did allow adding custom config to its webpack config, I would have preferred Workbox Webapck Plugin to achieve the same result like craco-workbox and the one suggested by @maciejsimka

Looks like Craco is similar to react-app-rewired. They are both very cool projects but you have to decide based on your requirements if adding another dependency would make sense. As react-scripts update, you'll have to take care that things don't break ...

Was this page helpful?
0 / 5 - 0 ratings