Federal Farmer

Rogue programmer. Yeoman homesteader. I hate the antichrist.

Adding PWA Support to Flask Applications

Progressive Web Applications (PWAs for short) are a growing trend in web app development, and for good reason.

lain_connected

No longer do programmers have to build, bundle, and ship separate mobile and desktop applications using kludgy bloatware like Electron or React Native. PWAs enable web apps to run in a native-like manner without having to write any additional code. Simply add some Javascript to your existing project and users can "install" your app on any major OS straight from their browser!

Not only does this significantly reduce development requirements for multi-platform support, it also allows developers to bypass the app store gestapo if they so choose. While PWAs can be bundled as executables for Android, iOS, Windows, etc. and syndicated through app stores, they remove the requirement of cowering before corporate gatekeepers to release your app.

Given the propensity of Google and Apple to block app submission for wrongthink, PWAs fill a much-needed hole in censorship-resistant app syndication.

I wish it didn't have to be this way - I wish the average consoomer were still capable of simply visiting a website directly through the browser or sideloading their own apk/exe files outside of the dreaded "App Store" monolith. But it is the current year and app-addicted smartphone fondlers account for 70+% of Internet traffic. They must be begrudgingly catered to and PWAs make this relatively painless.

Adding PWA support to a server-side rendered Flask application, while not difficult, isn't particularly well-documented. If you're starting a new Flask app and want PWA support out of the box, try using flaskeleton, a simple Flask project template by yours truly that supports Blueprints and PWAs by default.

Otherwise, consider this article your documentation ;)

Before we get started...

If you're using Flask solely as an API back-end and have a fully segregated front-end, this article isn't for you. It's very easy to add PWA support to most front-end frameworks when scaffolding these projects, so it's probably best to abide by the conventions of your framework's CLI or favorite npm package.

This article is for those who are using Flask, at least in part, to render server-side templates using the render_template function and Jinja2. This tutorial also assumes that you are familiar with Flask's blueprint design pattern.

Adding PWA Support in Flask

You don't need much to turn any web app into a PWA. Only four things are truly required for the Add to Home Screen prompt to be triggered on your site:

  1. HTTPS (or localhost) enabled
  2. A manifest.json file
  3. An icon set as defined in your manifest
  4. A service worker with a fetch event listener

This setup could be as simple as shoving your service worker registration into a <script> tag if you'd like. Since this is a Flask tutorial, however, we'll do this in as Flask-like a way as possible by using a blueprint to serve our PWA assets.

Our directory tree should look something like this:

├── app
│   ├── api
│   │   ├── __init__.py
│   │   └── routes.py
│   ├── __init__.py
│   ├── main
│   │   ├── __init__.py
│   │   └── routes.py
│   ├── pwa
│   │   ├── __init__.py
│   │   └── routes.py
│   ├── static
│   │   ├── css
│   │   ├── images
│   │   │   └── icons
│   │   │       ├── icon-144x144.png
│   │   │       ├── icon-192x192.png
│   │   │       ├── icon-48x48.png
│   │   │       ├── icon-512x512.png
│   │   │       ├── icon-72x72.png
│   │   │       └── icon-96x96.png
│   │   ├── js
│   │   │   └── app.js
│   │   ├── manifest.json
│   │   └── sw.js
│   └── templates
│       └── main.html
├── README.md
├── requirements.txt
└── run.py

(This directory tree and all examples in this tutorial are taken from flaskeleton, so please clone that repo if you need more verbose code snippets.)

Starting from the top of the tree, the first meaningful files are (fittingly) those in the pwa directory. Along with api and main, these are the three blueprints in our current project structure.

Make A PWA Blueprint

The routes.py file of our PWA blueprint should look like this:

app/pwa/routes.py

from flask import (
    Blueprint, make_response, send_from_directory
)

pwa = Blueprint('pwa', __name__, url_prefix='')


@pwa.route('/manifest.json')
def manifest():
    return send_from_directory('static', 'manifest.json')


@pwa.route('/sw.js')
def service_worker():
    response = make_response(send_from_directory('static', 'sw.js'))
    response.headers['Cache-Control'] = 'no-cache'
    return response

All this blueprint is doing for us is serving up the necessary static files for our PWA in a reusable way. manifest.json is being sent directly from our static directory, whereas our service worker at sw.js is being sent as a response with the no-cache header to request that clients revalidate it with each request.

We'll make these static assets in the next step.

Now that our blueprint is looking good, let's make sure it's properly registered in our app factory's __init__.py file:

app/__init__.py

from flask import Flask 

def create_app():
  app = Flask(__name__)

  from app.main.routes import main 
  from app.api.routes import api
  from app.pwa.routes import pwa

  app.register_blueprint(main)
  app.register_blueprint(api)
  app.register_blueprint(pwa)

  return app

That's all the Python we'll need to write! Our blueprint is serving up our static assets and the routes are properly registered with our app. Time to create our static assets.

2. Create Static Assets

The first asset we'll need is our sw.js file. It's very simple - just three event listeners:

app/static/sw.js

self.addEventListener('install', e => {
  console.log('[Service Worker] Installed');
});

self.addEventListener('activate', e => {
  console.log('[Service Worker] Activated');
});

self.addEventListener('fetch', e => {
  // e.respondWith(
  //  caches.match(e.request).then(res => {
  //    return res || fetch(e.request);
  //  })
  // );
});

Since all we're doing in this tutorial is trying to trigger the Add to Home Screen prompt, the bare minimum for PWA installability, our service worker is understandably simple.

Service workers are capable of way more events that enable functionality closer to a native app - push notifications, advanced caching strategies for offline use, etc. - that you might want to explore but are beyond the scope of this tutorial.

Next up is our manifest.json file, which basically serves as a config file for our PWA and includes things like an icon set of multiple sizes, display declarations for our app's splash screen, and basic description metadata.

app/static/manifest.json

{
  "name": "My Application",
  "short_name": "map",
  "description": "Describe your app here",
  "theme_color": "transparent",
  "background_color": "transparent",
  "display": "standalone",
  "orientation": "portrait",
  "scope": "/",
  "start_url": "/",
  "icons": [
    {
      "src": "images/icons/icon-72x72.png",
      "sizes": "72x72",
      "type": "image/png"
    },
    {
      "src": "images/icons/icon-96x96.png",
      "sizes": "96x96",
      "type": "image/png"
    },
    {
      "src": "images/icons/icon-144x144.png",
      "sizes": "144x144",
      "type": "image/png"
    },
    {
      "src": "images/icons/icon-192x192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "images/icons/icon-512x512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ]
}

You will, of course, need to populate the static/images/icons directory with icons of the appropriate size and name. Tools like RealFaviconGenerator can do this for you. The flaskeleton repo has example images if you need them.

The last static asset we'll need is a JS file that calls itself on pageload and actually registers our service worker:

app/static/js/app.js

(function() {
  if('serviceWorker' in navigator) {
    window.addEventListener('load', () => {
      navigator.serviceWorker.register('/sw.js')
               .then(function(registration) {
               console.log('Service Worker Registered');
               return registration;
      })
      .catch(function(err) {
        console.error('Unable to register service worker.', err);
      });
      navigator.serviceWorker.ready.then(function(registration) {
        console.log('Service Worker Ready');
      });
    });
  }
})();

This file, like sw.js, can be as complex or simple as you'd like it to be. You could add something like a button and custom event listener to create your own installation flow aside from the default Add to Home Screen prompt.

3. Serve Assets

All that's left now is to make sure our PWA assets are imported into our HTML templates!

<!-- PWA Assets -->
<link rel="manifest" href="{{ url_for('static', filename='manifest.json') }}">
<script type="text/javascript" src="{{ url_for('static', filename='js/app.js') }}"></script>

Since this is a Flask tutorial, we're using Jinja2's url_for functionality to import these files. You can import them in whatever template is most convenient - I usually stick 'em somewhere in the <head> tag but that's up to you.

Note that, since app.js is the file that's actually calling and registering our service worker, we're not importing sw.js here - just app.js and our manifest.

As we close out this tutorial, it's worth noting that in true Apple fashion, iOS does not play well with all aspects of the PWA ecosystem. They'll typically require specific icon sizes and <link> imports for full PWA support on iOS, and even then, PWAs are treated as second-class citizens by Apple products.

I'm not going to cover Safari and WebKit-based browser support here for this reason, but if you've made it this far, you can surely figure it out yourself ;)

Otherwise, congratulations! If you've done everything correctly up to this point, your web app should now be a PWA installable on Chrome-based browsers in Android, Windows, Linux, and macOS.

In Closing

A big thanks to this random user on Plebbit for succinctly covering generic addition of service workers to web apps, which helped when making this tutorial.

Yet more thanks to the maintainer of the flask-pwa package, which was also helpful in creating the service worker registration file. Like flaskeleton, flask-pwa is a blueprint-based Flask app template with PWA support out of the box, but with even more bells and whistles.

It works well, but I made my own because:

  1. flask-pwa includes some Heroku config files that most people don't need
  2. It uses Google's Workbox toolkit, an external dependency I'd prefer not to rely on
  3. It adheres to a Model-Template-Controller design pattern that I don't typically use
  4. There were some caching strategies being used by Workbox that really slowed my test PWA down

Developers of server-side rendered Flask applications now have two options to choose from when scaffolding new projects, though, which I think is a good thing.

Social Links

My Projects