Whatz the Good Word: PWA With Flask
This article shows how to build a simple vocabulary building game written as a progressive web application running on Flask.
Join the DZone community and get the full member experience.
Join For FreeIntroduction
Though I had not planned it initially, I purchased a domain name for my cloud server on which run the two Rails applications described in my previous DZone articles.
Some months ago, I had written a word game as a Flask web application. The code was inside my laptop, and I decided to put it out in the public domain. However, to give it a mobile app feel, I decided to make it a Progressive Web Application (PWA).
The first requirement for a PWA is that it should be served over HTTPS. My cloud server runs on Ubuntu which has snap pre-loaded, so the easiest way to switch to HTTPS is to run snap to install the utility certbot and run certbot for installing Lets Encrypt certificates and configuring Nginx. But Lets Encrypt certificates can be installed for domain names only, not for IP addresses. I did a quick search for domain names, and the cheapest one available was mahboob.xyz, which was perfect for hosting my side projects, so I bought it.
The commands I ran as root for enabling HTTPS are as follows:
$ snap install core; snap refresh core
$ apt remove certbot
$ snap install --classic certbot
$ ln -s /snap/bin/certbot /usr/bin/certbot
$ certbot --nginx
$ certbot renew --dry-run
For the IP address to domain name mapping, all I had to do was go into my domain registrar account and give the named servers of my hosting provider. In the hosting account control panel, I went to the Domains section and in just very few clicks the mapping was done.
PWA
As John Price explained in his article, How to Turn Your Website into a PWA, the advantages of a PWA are:
- Offline capable
- Installable
- Improved performance
- App-like experience
- Push notifications
- Discoverable
- Linkable
- Always up to date
- Safe
- Cost-effective
The real kicker is the last point. You don't have to invest time and money to write any mobile application code, whether native or cross-platform. Whatever you have used for your responsive web application is good to go; with minimal changes, it gets the features and feel of a mobile application.
My application is a simple word game. The user gets a clue and they have to guess the word. On a button click, they will be told whether they got the answer right or not. If the user wants to know the answer they will be shown the answer. If the user wants a different word, they will get a new clue for it.
The clues and answers are stored in a pipe-delimited flat file on the server. A couple of lines from the file are shown in the following screenshot.
When the Flask application starts, it reads the file contents into a data array. The root action is an index, which reads a random element from the array, splits it on the pipe character, and sends the first part (the clue) and the element's index to the game page. The clue is displayed as text and the index is kept as a hidden variable.
Buttons Functionality
- Check: It invokes the action method check, sending the index and the answer the user entered in the text field. The action retrieves the element from the data array using the index, splits it into pipe character and checks the second part (correct answer) with the user's answer. If they are the same, the method returns "You got it right!", else it returns "Wrong Answer! Please try Again!!". If the answer is correct, this button itself and the "Show Answer" button are hidden.
- Show Answer: This button invokes the action show, sending the index. The action method retrieves the element from the data array using the index, splits it into pipe character, and sends the correct answer back to the game page. After receiving the response from the server, the game page hides the input text field, Check and Show Answer buttons.
- New Word Clue: The functionality for this button is the same as the index. It invokes the action "new", which reads a random element from the array, splits it on the pipe character, and sends the first part (the clue) and the element's index to the page. The answer text field is cleared and the Check and Show Answer buttons are explicitly unhidden by calling the show method.
All the buttons make AJAX GET calls via JQuery.
PWA Requirements
In order to convert a web application to a PWA, there are three main requirements.
- Run it over HTTPS.
- Create and serve a manifest file in JSON format.
- Create and serve a JavaScript file to be registered as a service worker file.
My service worker JavaScript file is called serviceworker.js
. The game page registers it with the following code:
xxxxxxxxxx
if ('serviceWorker' in navigator) {
window.addEventListener('load', function() {
navigator.serviceWorker
.register('/wtgw/static/serviceworker.js', {scope: '/wtgw/'})
.then(function(registration) {
console.log('ServiceWorker registration successful with scope: ', registration.scope);
}, function(err) {
console.log('ServiceWorker registration failed: ', err);
});
});
}
I used the online Web App Manifest Generator to create manifest.json
. The manifest.json and serviceworker.js files are placed in the static folder, from which Flask serves public assets without requiring server-side routing.
The serviceworker.js file has event listeners for installing itself, opening the cache, activating the cache, and adding/fetching URLs and responses to/from the cache. It also handles two custom features in the fetch event handler.
- Getting a new word clue should not be cached. If it's cached, the same clue will be shown again and again from the cache. Preventing this is achieved by a check for the URL "https://mahboob.xyz/wtgw/new" and if yes, the code returns from the function, thus giving a pass-through to the server without checking the cache.
- If the user event has not been cached and the user is not connected to the internet, then the call to the cache returns with the response "You seem to be offline, please try after you're online."
The fetch event handler code is shown below: /static/serviceworker.js
xxxxxxxxxx
self.addEventListener('fetch', function(event) {
if (event.request.url === "https://mahboob.xyz/wtgw/new") {
return;
}
event.respondWith(
caches.match(event.request)
.then(function(response) {
if (response) {
return response;
}
return fetch(event.request)
.then (
function(response) {
if (!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
var responseToCache = response.clone();
caches.open(CACHE_NAME)
.then(function(cache) {
cache.put(event.request, responseToCache);
});
return response;
}
)
.catch(
function(err) {
return caches.open(CACHE_NAME)
.then(function(cache) {
return new Response("You seem to be offline, please try after you're online");
});
}
);
})
);
});
Deployment
On mahboob.xyz, the application is run by gunicorn, which requires the file wsgi.py, and is set up as a service. It is co-located along with two Rails applications. The application service and Nginx configuration are given below: /etc/systemd/system/wtgw.service
xxxxxxxxxx
[Unit]
Description=Gunicorn instance for MH Whatz The Good Word
After=network.target
[Service]
User=root
WorkingDirectory=/var/www/mh-wtgw
ExecStart=/usr/local/bin/gunicorn --workers 3 --bind unix:mh-wtgw.sock wsgi:app
[Install]
WantedBy=multi-user.target
/etc/nginx/sites-enabled/mh_sites
xxxxxxxxxx
server {
server_name mahboob.xyz;
passenger_enabled on;
passenger_ruby /usr/bin/ruby;
passenger_app_env development;
location / {
root /var/www/html;
}
location ~ ^/rbf(/.*|$) {
alias /var/www/rails6-bootstrap-flatpickr/public$1;
passenger_base_uri /rbf;
passenger_app_root /var/www/rails6-bootstrap-flatpickr;
passenger_document_root /var/www/rails6-bootstrap-flatpickr/public;
}
location ~ ^/cdb(/.*|$) {
alias /var/www/dashboard/public$1;
passenger_base_uri /cdb;
passenger_app_root /var/www/dashboard;
passenger_document_root /var/www/dashboard/public;
}
location /wtgw/ {
include proxy_params;
add_header Service-Worker-Allowed /;
proxy_pass http://unix:/var/www/mh-wtgw/mh-wtgw.sock:/;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
listen 443 ssl; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/mahboob.xyz/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/mahboob.xyz/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}
server {
if ($host = mahboob.xyz) {
return 301 https://$host$request_uri;
} # managed by Certbot
listen 80;
server_name mahboob.xyz;
return 404; # managed by Certbot
}
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
root /var/www/html;
return 301 https://$host$request_uri;
}
You can grab the code from the Github repository and play the game here. Have fun, and sharpen your vocabulary.
Opinions expressed by DZone contributors are their own.
Comments