This is the first in a 2-post series about how I built a Progressive Web App.
I have been intrigued about Progressive Web Apps (PWAs) ever since I first heard about them. At some point last November, I decided to build one, just to get a sense of what is involved and explore the possibilities the technology offers.
What better way to learn about a new technology than to build a project, right? I decided to build a real world project, and, in doing so, I learned about Progressive Web Apps and Golang at the same time. This is a 2-part blog post documenting the whole experience.
If you are curious to see what I have built, head over to https://dude-expenses.com. What you will see is a simple expense tracker built as a PWA with your typical Babel/Webpack/React/Redux (sigh) front-end app and the back-end server application written in Go. Feel free to create an account. I am using it all the time, but, hey, there are no terms of use, privacy policy or guarantees.
The whole application is open-source and currently lives in Github. Feel free to browse the front-end and back-end code. The app is deployed on a Digital Ocean droplet and Nginx proxies requests in front of the API (back-end) and the app (front-end).
Let us dive right into it starting with the front-end app.
PWwhat?
I think there is no point in explaining what a Progressive Web App is or does. There are others who have said it much better than I can, and I think that by now everybody pretty much knows what we are talking about. Heck, even the create-react-app now defaults to creating a PWA.
To sum it up, PWAs advertise speed, reliability and better user engagement. What really caught my interest about PWAs was the native-like promise for mobile devices. Let us face it, while desktop users can also benefit from caching, small asset footprints and offline functionality, the mobile domain is where a PWA really shines.
Resources
There is a ton of material on PWAs out there. Tutorials, how-tos, documentation, open source apps, you name it. Unfortunately, most of them are either too basic, too obscure, or just build a really simple toy app with no real usage.
What helped me the most was a series of blog posts by Addy Osmani, in which he goes deep into PWAs. As a bonus, the linked resources in his post are golden. The pwa.rocks website also has a lot of open-source apps, maybe more than is needed for a beginner. Another good resource is Jake Archibald's offline cookbook.
If you build it, they will come
Armed with knowledge after reading about PWAs, I went on and did the most illogical thing; instead of starting to build a PWA, I built a "standard" front-end application. Why? Good question. Well, it is the thing I know how to do.
I npm init
ed a project (yes, I like to start from scratch) and quickly added babel, webpack, webpack-dev-server, react, react-router, redux, redux-thunk, immutable and material-ui. Kept the dependency list small. The code-base contains nothing fancy. Your typical components, connected to a state tree with redux, react router handling the URL changes client-side and a simple service to talk to the back-end with fetch
. If you're interested you can see the full source code here.
The feature set is really limited. It includes a list of expenses filtered by month and grouped by day, creating new expenses, and editing or destroying existing ones. You can play with the app at https://dude-expenses.com.
OK, all set, the app is ready. We can always convert an existing app to a PWA, right? Hmmm...
PWA extreme makeover
Turns out you actually can, and I would actually recommend going this route. The most important thing that helped me during the conversion was having a checklist with the tasks required. I think that is the best thing you can do, if you are inexperienced in the field, like I am. What were the items on the list, and how did I complete them?
- Must be HTTPS
I used Let's Encrypt to obtain a certificate. Configured Nginx to use the obtained certificate and serve only via HTTPS by redirecting non-HTTPS requests. I even set up a cron job to auto-update the certificate when it is about to expire. Thank you, Let's Encrypt.
- Pages are responsive on tablets, mobile, desktop
Not much to say here, just make sure your pages show nicely on different devices. I went mobile-first, and I feel that the end-result is optimized for mobile. However, it is totally usable on all devices.
- Valid app manifest
OK, now we are starting to venture into PWA-land. This is actually easier than it seems.
The app's manifest is nothing more than a declaration of properties the app supports. You just need to create a manifest.json
file and include it in your HTML. The manifest plays an important role for the overall native feel of the app.
If you play your cards correctly and you provide different size icons for different devices, declare the app's start URL along with the name (short and long), orientation and main theme colour, the browser will show an "Add to home screen" installation banner on mobile devices. Once the user "installs" the app, they will have an icon in their device's desktop and the browser will ditch the address bar and other controls, provide an initial splash screen with your logo and color the browser "frame" with your theme. It is pretty sweet, trust me.
Oh yeah, and make sure you provide a cache buster for your manifest.json
file too. Once the browser downloads it and the user adds to home screen any change you make will not be picked up.
For the icons, I scourged the internets until I found something I liked and then converted it to different sizes.
To see an example of a manifest file, check the one I created. The "official" description of the manifest can be found here. You can see all of the available options.
- The start URL must load even when offline
The start URL should be specified in the web app's manifest. It is your application's homepage. You must provide meaningful content even when offline. You can leverage the power of the Service Worker API to achieve this. In short, a service worker is code that runs in the background. Its main responsibility here is to cache resources in order to serve them when the internet connection is lost (or flaky).
It is relatively easy to register a service worker and have it cache your resources. I ended up using the excellent sw-precache-webpack-plugin, adding the following configuration to my webpack prod config's plugins section:
new SWPrecacheWebpackPlugin({
cacheId: 'dude-expenses',
filename: 'sw.js',
minify: true,
staticFileGlobs: [
'index.html',
'bundle*.js',
'icons/*.png'
],
mergeStaticsConfig: true
})
This basically tells the plugin to create a minified sw.js
file and include it in index.html
. The code registers a service worker and tells it to cache all staticFileGlobs
under the "dude-expenses" cache key. The static files I am caching here are the app's homepage, the webpack bundle, and the icons. There is no golden rule here, you can cache whatever you feel is needed to provide your user with a great experience.
You can see my full webpack configuration for the project. This is the simplest possible usage of a service worker. There are many more things you can do, but we will touch upon them later on.
- Each page has a URL
- Site works cross-browser
- First load fast even on 3G
I think the 3 above items are general guidelines when building web apps. While they are "required" to pass the PWA test, there is nothing PWA-specific about them. Just make sure you are not using any fancy javascript feature, your UX is well thought out, and you do not have a ton of resources for the browser to load. Compress your assets and think twice before using that hot new JS module, and you are good.
The above completes the minimum required items you need to take care of to pass the PWA test. There is a heap more stuff, all basically being guidelines to achieve speed, performance, a clean UX, and satisfy Google. Please see the complete checklist. It is worth it.
Tools of the trade
After compiling the list of things to implement for my PWA, I felt overwhelmed. There is a ton of stuff in the list. How could I be sure I do not leave anything out? Where do I start? How do I know that a list item is indeed completed?
The tool that helped me the most here is Google's Lighthouse tool. You can either use it from the command-line or install a Chrome extension. I went for the latter.
The Lighthouse extension usage is really simple. You navigate to a page in your browser and then activate the extension. Lighthouse takes control of your browser, runs an audit of the website and generates a report with a score and a progress indicator.
What is really helpful is that, apart from breaking down each PWA checklist item and marking it as "pass" or "fail", it actually provides help or suggestions on how to fix the "failed" items. Armed with the Lighthouse extension, I managed to complete all items, one by one, without worrying if something would break. It felt a bit like TDD. I would highly recommend it.
Benefits
Once my PWA was complete, I decided to take it for a spin. I accessed the app from my phone. It is a standard Android phone, kind of old. Nothing special.
I fired up the Chrome browser and visited the page. It was nice to see a coloured address bar. I started using the app with 3G. It felt snappy (even though I have not added any loaders). So far, so good, but no real benefit from a PWA.
As soon as I visited the app's URL a bunch of times, things got interesting. Chrome showed me a banner to "install" the app to my Home screen. Once I accepted, I got a nice shortcut for the app in my phone's Home screen. All information here was taken from the app's manifest file.
Opening the app from the shortcut yielded another nice surprise. The app was accessible from a stripped-down Chrome, free of clutter and address bar, themed according to the theme colors I specified in my manifest.json
. While the application was bootstrapping, I got a nice splash screen with my icon and app title. Once I used it a couple of times, the app loaded even faster, thanks to the caching of assets done by the service worker.
Converting an app into a PWA does indeed give a sense of native look and feel on mobile and tablet devices.
More PWA!
In this project, I basically scratched the surface of what can be done with PWAs. I think that I took complete advantage of the manifest features, but there is definitely great room for improvement in the service worker area.
First of all, the app could be made to work offline 100%, allowing the user to work without relying on an internet connection. Offline-mode has two sides. The first is straight-forward; you cache your HTML, CSS, and just a bit of Javascript to render something meaningful to the user. Then comes the fun part.
The data that the application uses is actually a cache-able resource as well. This means that, with the Service Worker API, the browser can fetch data whenever an internet connection is established and then read from the cache when the device is offline. With a simple notification that the data might be stale, the user probably will not mind to see outdated data. This effectively covers all GET
requests that your app might make.
POST
requests can be handled too. In my app, I could record all API requests when offline, serialize them, store them in the browser using the Web Storage API, and, once the device comes online, replay all requests to the server. This way, the app will be 100% functional, even when the device is offline.