Writing a PWA with Aurelia
Posted on 2019-09-01 in Aurelia Last modified on: 2022-09-11
I feel like Progressive Web Application (PWA for short) are a more and more popular way to build mobile apps. That's understandable since you can use the same technology you use to build a SPA to build one! So you stay in known grounds, can use your usual web browser to develop and test the app. And, unlike hybrid apps, you also enhance your standard SPA! In this article, I'll explain how I build a PWA named aurss with the Aurelia framework. I'll start by explaining what PWAs are and some specific tech you must use to make them work. Then I'll explain how to build one with Aurelia.
PWA basics
Simply put, PWAs are "installed" web pages: when you browse on a website that is a PWA, you will be prompted to install it. If you accept, you will see the icon of the APP on your home screen (like any other app) and if you press this icon, the PWA will launch in full screen mode just like a native app. But you didn't go to the app store to install it.
As a developer, building a PWA means:
- Your users don't have to install it: they can use the app directly on the website without installing anything and you'll still be able to provide them advanced features like caching.
- You will have more control on when you can update it: no need to submit it to the app store and wait for validation. Just push files to you server. The update will occur the next time your users reopen the app.
- You are not bound by the rules of any app stores.
- The app will work on any device with a recent browser. Chrome has the best support for PWAs right now, Firefox is quite good but Safari support can be an issue (even if it's getting better, see this article for instance).
- The app can be progressively enhanced (eg by adding better offline support or better caching).
- You show notifications the user with the Notification API and the Push API.
- You can access some native features (like the camera or the microphone).
Here are the basic components needed to make a PWA:
manifest.json: it's a JSON file describing how the app must be displayed and installed. You'll use it to specify the name, icon, orientation and some display properties of your app. See here for more details. It is required to make your app a PWA.
ServiceWorker (SW for short): it behaves like a proxy server between your app and the network. It will keep working even when the tab is closed. So it will allow you to:
- Have caching and offline support.
- Sync things in the background.
- Subscribe to notifications of the Push API and display them to users.
Please note that:
- It only works over HTTPS (except for localhost).
- It cannot access the DOM.
- It may apply to all or only part of the pages.
- It must be registered to have an effect.
- It will only get updated if its content changed and after the tab is closed and reopen (unless you use the dev tools and force it to reload on change).
See here for more. It is required to make your app a PWA.
indexDB: where you put data to exchange with the SW (SW cannot access localStorage). See here for more information.
Many other APIs I'll just list here so you know what you can do:
- Notification API to display notification to the user.
- Push API to receive messages from a server.
- Many more: geolocation, camera access, …
The App
Now that we've seen what the basic components are, let's see how to do this with Aurelia.
In a nutshell, you want to build a normal app while paying attention to some details, relating with the SW: it will rely on indexDB for storage, on the fetch API for network (luckily that's what the Aurelia HTTP client is using), all its APIs are promise based and it will be reinstalled only when it changes.
I assume you are familiar with Aurelia so I won't explain Aurelia specific things (since they don't exist, I just build a standard app using the framework) and focus on PWA stuff. Anything I explain here is applicable with a basic app created by the CLI (if possible use webpack since this article will be easier to follow if you do).
So to make your app a PWA, you will have to:
- Add the manifest.json.
- Add the service worker.
- Check with lighthouse that the app is installable.
You can then add other capabilities if needed.
manifest.json
Once you've build the app, you can add the manifest.json file. To do so add in your index.html or index.ejs:
<link rel="manifest" href="/manifest.json">
Then in the static/ folder (if you are not using webpack as configured by aurelia-cli, you may need to adapt this, the goal is to have this file in the root of the build files next to the index.html file), you can create the manifest.json file. Below is an example, you can adapt it to your needs. It also has more properties, see the documentation to view all its possibilities.
{ "name": "aurss – RSS reader", "short_name": "aurss", "start_url": "/", "scope": ".", "display": "standalone", "background_color": "#FFF", "theme_color": "#493174", "description": "RSS reader for TTRSS", "dir": "ltr", "lang": "en-US", "orientation": "portrait-primary", "icons": [{ "src": "/icons/aurss.96x96.png", "type": "image/png", "sizes": "96x96" }, { "src": "/icons/aurss.512x512.png", "type": "image/png", "sizes": "512x512" }] }
ServiceWorker
Then, include the service worker:
Create a file named sw.js in your src folder. Leave it empty for now.
Register the worker in the app.js with:
if ('serviceWorker' in navigator) { runtime.register() .then((registration) => this.logger.info('Service worker is registered', registration)) .catch((registrationError) => this.logger.error( 'Service worker failed to register', registrationError, )); } else { this.logger.info('Service worker is not available in this browser.'); }
Make sure it will be copied when you build the application next to the index.html. For instance, with webpack, you can use this plugin (which will also be handy later for caching so I recommend that you use it or something similar).
Note
Even you are using TypeScript, I suggest you rely on plain JavaScript for the SW file. It will be much easier to write and you won't have to worry about transpilation.
Astuce
If you are using Chrome, the Application tab of the dev tools is really useful. It will show you information about the manifest file, the service worker and the used storage.
Astuce
ES6 imports won't work with SW, you must use something like importScripts('/src/js/idb.js'); to import things.
Now the SW is bootstrapped, we can start to use it to:
Cache the shell of the app (ie the files it needs to work) and provide offline support. This is done once the SW is installed, so when the installed event has been fired (self represent the SW instance):
// Name of our static cache. const staticCachePrefix = 'static'; // Update this each time you app shell changes. // This way, the SW will have changed and get reinstalled // allowing for the new files to be cached. const VERSION = '1.0.0'; const staticCacheName = `${staticCachePrefix}-${VERSION}`; self.addEventListener('install', (event) => { log('Installing SW version:', VERSION); // waitUntil is there to make the sure SW is neither paused nor stopped // while we expect it to do some work until the promise we passed to it completes. // See: https://stackoverflow.com/a/37906330 event.waitUntil( caches.open(staticCacheName) .then(cache => { console.log('Caching app shell'); // serviceWorkerOption comes from serviceworker-webpack-plugin. // Adapt if needed. cache.addAll(serviceWorkerOption.assets); }), ); });
You can then use in your app.js file, in the constructor, something like the code below to display notification to the user about online/offline status:
this.onOffline = () => this.store.dispatch(isOffline.name); this.onOnline = () => this.store.dispatch(isOnline.name); if (!navigator.onLine) { this.store.dispatch(isOffline.name); } else { this.store.dispatch(isOnline.name); } window.addEventListener('offline', this.onOffline); window.addEventListener('online', this.onOnline);
Catch network requests, for instance to implement dynamic caching.
self.addEventListener('fetch', (event) => { // Let the browser do its default thing // for non-GET requests. if (event.request.method !== 'GET') { return; } event.respondWith( caches.match(event.request) .then((response) => { return response || fetch(event.request); }), ); });
When offline, you can store data in indexDB to send it as soon as the browser gets online again. To do this, you must listen to the sync event in the SW. You can take inspiration from this code for the SW part and from these utility functions (see a usage here) to achieve this.
Note
There are many caching strategies: with network fallback, cache only, network only, network with cache fallback, cache then network. I won't detail them here. Search on the internet if needed.
Note
Don't forget to clean the cache from time to time. This can be done with:
self.addEventListener('activate', (event) => { console.log('Cleaning old cache shell'); event.waitUntil( caches.keys() .then((keylist) => Promise.all( keylist .filter((key) => key !== staticCacheName && key.startsWith(staticCachePrefix)) .map((key) => caches.delete(key)) )), ); });
Finally, you should test you app with lighthouse to check it actually is a PWA. The tool will also check for performance which is a good thing. In the Chrome dev tools, go to the Audit tab, verify that Progressive Web App is checked under the audit section and run the audit. The report should tell you that you have indeed created a PWA (and if not, what you are missing).
Conclusion
To sum up:
- PWAs are not tied to any framework. You can make a PWA without one or add PWA capabilities to a SPA written with any framework.
- They are quite fast to develop and allow for good code re-use because you are not making an app distinct from you main web site.
- You don't have to learn many new technologies, most of the things you will do will be standard we development.
- You can progressively enhance an app with PWA capabilities.
- Pay attention to what they support and what they don't before starting and to iOS support.
Resources
- My test application: aurss It's MIT licensed so you can re-use it. Don't hesitate to ask me questions in the comment or in Gitlab.
- A course I took (I highly recommand it)