The goal was to create a PWA but with functionalities and behaviour of a real native app (because a normal PWA would be too easy of course). After a brainstorm, we came up with a simple application for Facebook users who want to take back control of what they see in their feed. That’s how FeedGist was born – an application to create the perfect feed, only containing posts from Facebook Pages users want to see. No interruptions, no ads, and no fluffy kittens. We also wanted to use Push Notifications in a funkier way than traditional implementations. Users control push notification frequency and get alerts with the number of posts they’ve missed since last login. Pretty cool right?
CHECK OUT THE CODE FOR YOURSELF ON GITHUB
HOW DID WE CREATE IT?
We decided to split the project into 2 layers. The first, a very simple API to authenticate users via Facebook, display user’s liked Facebook Pages, give the function to select and save, read posts, and automate sending Push Notifications. The second, a front-end PWA layer that calls our API to render the data.
Our API has been based on the latest Laravel Framework (at the time of development that’s 5.4). Push Notifications are done thanks to OneSignal. Besides push notification sending functionality, they provide an awesome Admin Panel to look at registered users, with click statistics and the ability to send some customized message to single user. Neat!
Our front-end layer has been created on top of the Angular Framework, using Angular Material Design’s UI components and all the goodies from sw-precache to support the offline mode of the application. We decided to use Service Worker Toolbox library to support working app with unavailable or poor network connections.
Our source code has been uploaded to GitHub so you can see how it works under the hood, but this article is focussed on the features of Progressive Web App technology, so we’ll focus on our front-end layer.
ANGULAR 2…NO 4
We are huge Angular fans and have successfully used it in many web/app projects. In Angular 2, there’s a perfect tool called Angular CLI that makes it really easy to start creating an application that ‘just works’, right out of the box. It helps in generating components, routes, tests from the command line and so much more. A huge help and time saver! The Angular team are working hard on another tool called Angular Mobile Toolkit too. It automatically integrates all the progressive features i.e. offline capability or mobile-network-friendly tools. Unfortunately, it is not production-ready yet so we had to come up with our own solution.
In other words, every time you run an Angular application, it is being compiled using the Just-in-Time (JIT) compiler. It is good when we - as developers – are in the dev phase, but not really cool if the application flies to production, when we need maximum optimisation to reduce page load time.
Angular obviously thought about that and that is why they came up with Ahead-of-time compilation that we can run before application deployment. Thanks to this solution we have faster rendering, fewer asynchronous requests and smaller framework download size. We reduced the total asset size by 45%. Now the Angular team have decided to enable AOT compilation as default executing ng build —prod. Previously we had to run ng build —prod —aot. Double awesome!
Service Worker is a script that runs in the background of a browser and saves application data in its memory. Thanks to this, offline support is now possible. As well as the obvious benefit of offline support, online devices also benefit. By using special handlers, we can fetch scripts and images from Service Worker directly, without any requests to server (caching runtime requests), making things a whole heap faster. Service Worker enables Push Notification support too. I have a feeling we’ll all be getting a load more notifications in the future so take advantage of this whilst it is fresh!
To register a Service Worker, we simply put a <script> that is responsible for that:
sw-registration.js hosted with ❤ by GitHub
There was no need to write our own Service Worker (service-worker.js) from scratch, sw-precache is a perfect solution recommended (and developed) by Google and pretty much the whole industry. There is also another tool (also made by Google), sw-toolbox.
CACHING TOOLS EXPLAINED
sw-precache is a module for generating a service worker and is responsible for caching the „app shell” - i.e. local resources that web app needs to load its basic structure. Local resource is an app CSS stylesheet, plus a couple of images inc. logo and so on. Generally, it refers to local files.
sw-toolbox is a module for caching resources that exist independently from the app shell - in our app these are Google Fonts or our own API exposed on external URL. Generally, it usually refers to dynamic content i.e. 3rd party data, but sometimes to first-party data too that is dynamically generated or frequently updated.
sw-precache generates a service worker as we already mentioned, but it also generates a hash of each file, and once some file has been updated, it re-generates the hash and service worker knows about the changes in the app and re-installs the service worker to re-fetch only the updated resources. Breath! So, if we updated our application background color and we run the build process, service worker was re-generated and hash of the SCSS file responsible for app’s background color was updated. Then if we went to the application, we could see the changes because the one CSS file was updated.
sw-toolbox and runtime caching
We use many external resources that are loaded dynamically in the application i.e. images from Facebook, fonts from Google Fonts or our API accessible under different URL (https://api.feedgist.io). To make them executable even in offline mode or with slow internet connection, we had to think how to execute our API so the results are always the most up-to-date. For example, Google Fonts only executes only from cache as it would never change.
sw-toolbox is great and it was a total breeze to configure it:
sw-toolbox-config.js hosted with ❤ by GitHub
Each URL pattern has its own caching strategy - we decided to use the networkFirst strategy for our API and OneSignal URLs as these are being constantly changed, and we always need fresh data. Google Font files or Facebook Images should be retrieved always from cache if the cache entry exists, otherwise the browser tries to fetch the resource from the network.
That’s how we support offline mode in our application! Simple huh.
OTHER THINGS TO REMEMBER WHEN CREATING A PWA
- Secure connection PWA must be under https://.
- Application is progressively enhanced PWA looks and works good on every browser, on every screen resolution.
- Add To Homescreen functionality If we want to give user app-like experience, it is obvious that application should be executable by clicking the icon on homescreen. To achieve that, manifest.json is a file that comes to our saviour. Unfortunately, it works only on Android devices. On iOS you can add an application to home screen from Safari, but it requires additional click-through operation. BOOO APPLE!
- Applications icons Are needed and all of them should be specified also in manifest.json file. All the settings are self-explanatory so feel free to look into our FeedGist manifest.json and use it as your reference.
- Load time performance measurement tool As we already mentioned - it is one of the most important points on the PWA checklist. To measure the performance we use Lighthouse - an open-source auditing tool, which is available as a Chrome Extension or Node CLI tool.
SERVICE WORKER UPDATE PROCESS
Because of caching application resources in the App Shell, once some update of application is deployed to the server, the user will not see the changes immediately. The application is loaded and once it’s done, the service worker will listen for changes and will detect the updates and in fact on the 2nd visit, user will load the changes. We decided to do auto reload of application.
We needed a simple and easy implementation solution that would allow us to send Push Notifications automatically detailing the number of unread posts on FeedGist, at the hour selected by the user since the last visit. Implementation was easy and well-documented on OneSignal website.
We only added a script into <head> section of index.html to start using Web Push SDK.
Everything went fine, until we faced these problems…
PROBLEM #1: SERVICE WORKER GENERATED BY SW-PRECACHE AND ONESIGNAL SERVICE WORKER
A browser can register only one Service Worker file. Service Worker generated by sw-precache, responsible for our app offline support, caching resources, did not work in parallel with OneSignal, that has its own Service Worker responsible for sending Push Notifications. There is a notice on OnSignal’s website:
Our service workers OneSignalSDKWorker.js and OneSignalSDKUpdaterWorker.js overwrite other service workers that are registered with the topmost (site root) service worker scope.
So even if we merged the files somehow, OneSignal Service Worker would be at the top and our own Service Worker would be overridden and not be working. Argh!
Solution #1 We decided to save service-worker.js file (generated by sw-precache) as we used to, but not to register this service worker in index.html. Instead we registered OneSignalSDKWorker.js file (required by OneSignal), but with imported service worker (the one generated by sw-precache):
import-sw.js hosted with ❤ by GitHub
Unfortunately the imported scripts seemed to be cached forever i.e. when we did an update in any JS or SCSS file and our service-worker.js was re-generated (with new hash of changed files), the browser did not reflect the changes and service worker did not sync the changes we did. The result was bad - we were not able to update the application any more.
We started to try to google something related to this issue and we found something on the official GitHub Issues pages of sw-precache. Because our server is an Apache Server and we enabled HTTP caching of files server-side, it looked like the browser was not aware of our changed file(s) (service-worker.js) because it was cached. We excluded that file from caching and to be safe, all OneSignal files too, setting following rules in our .htaccess file:
.htaccess hosted with ❤ by GitHub
Thanks to this, all of those files started to be retrieved fresh from the network all the time. Unfortunately, it still did not help… Although the browser correctly saw a fresh copy of the JS files (inc. service-worker.js), the registered OneSignalSDKWorker.js was not perceived as updated Service Worker for browser (it looks like browsers do not reflect changes in imported scripts).
Solution #2 After a few hours of testing different workarounds, we decided to automatically add some hash in OneSignalSDKWorker.js Service Worker with each update of our Service Worker i.e. importScripts(‚service-worker.js?v=0.242345129’);- just for browsers that would start seeing any update of real registered Service Worker.
To achieve that, we created our own script (using replace script via NPM to replace „service-worker.js” string in OneSignalSDKWorker.js with "service-worker.js?v=" + Math.random()
In package.json we registered the script to be executable from command-line:"sw-update": "node sw-update.js".
Now after each build process, we run simple npm run sw-update and OneSignalSDKWorker.js imports our service-worker.js with random string and the browser reinstalls the Service Worker because it perceives it as an updated file. BOOM!
PROBLEM #2: BROWSER CACHE STORAGE CLEAR + PWA TESTING
If you use Chrome browser and you develop an application that uses Service Worker and you do some tests, you need to remember that in case of clearing browser cache storage and unregistering service worker - simple click of „click site data” in Chrome does not really help at all and you’ll probably still able to see an unchanged project (cached).
It took us 4 hours to find out about it. What a nightmare!
PROBLEM #3: SERVICE WORKER SUPPORT
It is not a secret that Progressive Web Apps are not fully supported on iOS. The biggest pain is lack of Service Worker support so running an application in Flight Mode (offline) will not show the cached version. We have to forget about Push Notifications too. Not ideal but it does not mean it is not worth the effort!
PWAs DO work on iOS. Hopefully Apple will pull their finger out and put user experience before their app store, but we’ll see. Surely they can’t ignore this movement.
Browser support is quite good and you really should consider PWA feature implementation. Even if not all features of PWA benefit all users right now, they will get a better experience and your conversion rates will/should/maybe/hopefully/definitely improve (thought we should not promise anything just in case your app is, well, shit!).
PROBLEM #4: IOS AND LOCALSTORAGE
This problem is not related to PWA and its support but directly to iOS. On my iPhone, once PWA is launched from my homescreen, it opens the application in full screen (which is expected behaviour), but it looks like the app is opened in Private Mode. What does it mean for FeedGist!?!?
Problem. Login with Facebook functionality and storing JWT token in storage requires Local Storage which does not work in that mode. Browser returns an error:
We decided to get rid of below code responsible for opening application in full screen mode on iOS:
Now, when user clicks FeedGist icon on their home screen, our application opens in a browser rather than in full screen mode. Unfortunately, it is the only workaround for now.
I hope you find this case study useful and you enjoy developing your own Progressive Web Application. I believe the support is only going to get better and the future of applications lay in PWAs. Who knows, perhaps they’ll overtake app store downloads one day. Searchable on Google, no download, great UX, what is not to love?
I’d love to know your thoughts and if you are a company looking for your very own Progressive Web App, you know where to come!