Marcus Hellberg

PWA and offline

In the first four tutorials in this series, we’ve built an app with LitElement, using Redux for state management, Vaadin Router for navigation and Webpack for building and code splitting.

In this final tutorial, we turn the application into an offline-capable Progressive Web App.

If you didn’t do the previous steps of the tutorial, you can check out the code from the previous step as a starter:

Tip
You can find out more about Progressive Web Apps on Vaadin’s PWA page.

Video tutorial for this part

Adding a Web App Manifest

The first step in turning an app into a PWA is adding a manifest file to identify the app to the browser.

In the src folder, create a file manifest.webmanifest:

src/manifest.webmanifest
{
  "name": "Todo App",
  "short_name": "Todo",
  "start_url": ".",
  "display": "standalone",
  "background_color": "#2A3443",
  "description": "LitElement tutorial",
  "theme_color": "#2A3443",
  "icons": [
    {
      "src": "./img/icons/icon-192x192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "./img/icons/icon-512x512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ]
}

The manifest file collects all the relevant info about your app into a single file. Link the file from index.html:

src/index.html
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Todo</title>
+    <link rel="manifest" href="./manifest.webmanifest" />

Add the manifest file to the list of files Webpack copies over during the build:

webpack.config.js
const assets = [
  {
    from: 'src/img',
    to: 'img/'
  },
+  'src/manifest.webmanifest'
];

Adding a ServiceWorker

The second step in turning an app into a PWA is adding a ServiceWorker. The ServiceWorker is a script that sits between your app and the network that can be used to cache and serve resources, so the app loads fast and works reliably regardless of the network conditions.

In this project, we are using Workbox for generating the service worker. It is a widely used library for building production-grade ServiceWorkers. The project Webpack configuration uses the Workbox plugin to automatically generate a ServiceWorker that caches and serves all the static assets.

Open sw.js in the src folder.

src/sw.js
if ('workbox' in self) {
  workbox.precaching.precacheAndRoute(self.__precacheManifest || []);
}

Webpack only adds a ServiceWorker when running the prod or dev:sw scripts. Detect whether or not workbox is loaded. If it is, call precacheAndRoute with self.__precacheManifest which is an array of assets generated by the build script.

Load the ServiceWorker file in index.js:

src/sw.js
window.addEventListener('load', () => {
  initRouter();
+  registerSW();
});
src/sw.js
async function registerSW() {
  if ('serviceWorker' in navigator) { (1)
    try {
      await navigator.serviceWorker.register('./sw.js'); (2)
    } catch (e) {
      console.log('ServiceWorker registration failed. Sorry about that.', e);
    }
  } else {
    console.log('Your browser does not support ServiceWorker.');
  }
}
  1. Ensure that the browser supports ServiceWorker before trying to register it.

  2. Register the ServiceWorker. Note that the path of the ServiceWorker determines its scope.

Start the app with the ServiceWorker enabled, and you should see the loaded ServiceWorker in the "Application" tab of DevTools. You should also be able to go offline and refresh the page and verify that the app still loads.

$ npm run dev:sw
Going offline
Figure 1. Going offline

Saving the state to localstorage

The application can now start offline, but it doesn’t persist the todos between runs.

Because the state of the application is in a central Redux store, we have a clean way of persisting and loading the state in localstorage.

Start by adding functions for saving and loading the state in store.js.

src/redux/store.js
const STORAGE_KEY = '__todo_app__';

const saveState = state => {
  localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
};

const loadState = () => {
  const json = localStorage.getItem(STORAGE_KEY);
  return json ? JSON.parse(json) : undefined;
};

Then, load the state from localstorage when creating the store:

src/redux/store.js
export const store = createStore(
  reducer,
+  loadState(),
  window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);

Finally, subscribe to the store to call saveState on any state changes.

src/redux/store.js
store.subscribe(() => {
  saveState(store.getState());
});

Now, if you rerun the application, you should be able to see the application state persists between refreshes.

State persisted between refreshes
Figure 2. State persisted between refreshes

Conclusion

This five-part tutorial has walked you through building a PWA with LitElement and Redux.

You can find the finished code in GitHub:

Further reading

Here are some helpful resources for continued learning.

Let us know what you think!

As always, if you have questions or suggestions, feel free to comment below!

Vaadin is an open-source framework offering the fastest way to build web apps on Java backends
GET STARTED

Comments (3)

Marcus Hellberg
3 years ago Jan 16, 2019 9:23pm