This article was originally published on jdkaplan.com and is republished here with permission.
In this article we'll build on my previous article, Web Development Basics with Python & Flask and introduce the Javascript language and the concept of asynchronous communication with a webserver to provide a clean and dynamic user experience.
If you haven't read my last article on the topic or aren't familiar with building a simple web server with Flask, I'd encourage you to start there.
A Starter Framework
Let's begin by building our basic Flask web application. We'll start with our HTML. The HTML tells the browser how to render elements on the screen and ultimately what the user will see.
<!DOCTYPE html>
<html>
<head>
<title>Sample App</title>
<style>
*, table * { font-family: 'Courier New', monospace; }
</style>
</head>
<body>
<h1>Random Data:</h1>
<table>
{% for dp in data %}
<tr>
<td><b>Sensor {{loop.index}}: </b></td>
<td id="dp-{{loop.index}}">{{dp}}</td>
</tr>
{% endfor %}
</table>
</body>
</html>
In the above snippet, we use a syntax specific to our Flask
application (the highlighted lines) to loop over some data
(more on this in a moment using the {% for i in something %}
syntax and printing
template date using the {{x}}
. This syntax uses a templating engine called
Jinja. I won't get too far into
the specifics here, but the gist is that Jinja is used by Flask to allow you to
pass data into your HTML templates and then do dynamic things like loop over that
data and format it.
Just know that the items wrapped in curly braces ({}
) above
are part of the Jinja templating engine, not part of HTML.
Now lets write our Flask webserver and see how we pass this data into the HTML template.
import random
import json
from flask import Flask, render_template, Response
app = Flask(__name__)
def read_sensors():
"""Returns an array of 3 random values between 0 and 1"""
return [ f'{random.random():.4f}' for i in range(3) ]
@app.route('/')
def index():
sensor_data = read_sensors()
return render_template('index.html', data=sensor_data)
if __name__ == '__main__':
app.run(host='127.0.0.1', port=5000, debug=False)
We have a few things going on here. First, we defined a read_sensors()
function
that generates a list of three random floating point values and returns them as
a list of strings. This is meant to simulate reading data from a sensor.
Second, we define our /
route which maps to a functiona index()
. This function
"reads" from our sensors and renders our index.html with the sensor data passed
in to the template as the data
argument. This is how we have access to the data
that we can loop over in our HTML template.
Running our webserver by running python app.py
, we can view our application
at http://localhost:5000.
If we refresh the page, we'll see our random data refresh as well. In the next section we'll introduce Javascript and the concept of asynchronous interaction with a web server.
Asynchronous Javascript Concepts
Javascript
Javascript is a programming language with origins and evolution closely tied to the web. It runs in web browsers. Though the world of Javascript has evolved to allow Javascript to run on servers, IoT devices, and all sorts of applications using platforms like Node.js, for now we'll focus only on how Javascript applies to the web when running in-browser.
The goal of this article is not to teach you Javascript in detail. To dive deeper into understanding the language (and I do recommend you dive deeper into the language), there are numerous sources of documentation and tutorials including W3 Schools, MDN Web Docs, and freely available college courses such as Harvard's CS50. One of the most useful books I've read on the topic is You Don't Know JS, which deep dives into how the language works.
Syntactically, Javascript is similar to other C-inspired languages. A for-loop looks like:
for (let i = 0; i < 10; i++) {
console.log(i);
}
If-else statements similar to C or Java:
if (myVar === 'hello') {
doSomething();
}
else {
doSomethingElse();
}
Note that equality comparisons in Javascript often use ===
rather than ==
. The
three-equals is more common and is generally considered safer because it
compares both the type and value of a variable.
Javascript has the same programming concepts as most other languages including functions, objects, switch statements, etc. Again, my intent here is not to teach the details of the language, but to give a brief introduction. I strongly encourage you to dive deeper into the language on your own.
AJAX
Ajax stands for Asynchronous Javascript and XML. The name is a bit historical and specifically references the XML data format, which is an extension of HTML. While it's still common, it has become less popular in the context of Javascript over time. For now, simply think of the term Ajax as synonymous with "asynchronous calls to a webserver".
What is an API?
An API or Application Programming Interface is another term that has seemingly outgrown its original definition a bit. In the context of the web, an API generally refers to a set of available routes that a web application exposes for interacting with a web server. An individual API endpoint simply refers to an individual route.
The main difference between an API endpoint and a route is that routes can return HTML, data, or anything else that's valid within the scope of HTTP. But an API specifically refers to a route that's meant to be used by another program and generally returns structured data in a well-defined format.
JSON
JSON (JavaScript Object Notation) is a data format that is the de facto standard for web APIs these days. It's certainly not the only format for structuring data, but it has become very widely used due to its native presence in the Javascript language and its ease of interoperability with other languages such as Python.
JSON data consists of key-value pairs. For example:
{
"foo": "bar",
"x": 10
}
JSON supports the following data types:
string
- It's a string. There's not much else to say.number
- These can be integers or floating point numbers. This is because Javascript doesn't differentiate between the two. All numbers in Javascript are 64-bit floating point values.boolean
- This can betrue
orfalse
.object
andarray
- The values in JSON data can be more nested JSON.null
- A value can also be null.
The example below shows each of these data types.
{
"myString": "this is a string",
"myNumber": 10,
"pi": 3.14159,
"myBoolean": true,
"anObject": {
"look": "moreJSON"
},
"anArray": ["foo", "bar"],
"somethingMoreComplicated": {
"a": {
"b": false,
"n": true,
"t": true
},
},
"arrayOfObjects": [{
"id": "1"
}, {
"id": "2"
}]
}
In the interest of something a bit more practical, consider this example representing user data:
[
{
"username": "user1",
"email": "user1@example.com",
"first_name": "User",
"last_name": "One",
"admin": true
},
{
"username": "user2",
"email": "user2@example.com",
"first_name": "User",
"last_name": "Two",
"admin": false
},
{
"username": "user3",
"email": "user3@example.com",
"first_name": "User",
"last_name": "Three",
"admin": false
}
]
This example uses an array of objects that contain multiple data types to represent users of a web application.
Ajax in Practice
Adding a New Data Route
Before making any ajax calls to our webserver, we need to have an API endpoint (or route)
that returns our data. We can add that to our Flask app by adding the following to our app.py
file.
@app.route('/data')
def get_data():
data = read_sensors()
return Response(json.dumps(data), mimetype='application/json')
This creates a route at /data
that reads our random data from our mock sensors
and responds with a JSON array of the strings containing the data.
Why strings? While using numbers would have been a valid choice here, using strings to represent numbers when sending data between systems (i.e. between your server and your browser) can be a good practice to avoid truncation or precision errors resulting from different platforms or languages. In this case, we ultimately want the number as a string anyway, so it's convenient.
This is visible in the browser by navigating to http://localhost:5000/data as shown above.
Including jQuery
To make asynchronous calls to our server, we need a library to help us do that. For this article, we'll use a library called jQuery. To include jQuery in our project we need to update our HTML template.
<!DOCTYPE html>
<html>
<head>
<title>Asynchronous Example</title>
<style>
*, table * { font-family: 'Courier New', monospace; }
</style>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"
integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4="
crossorigin="anonymous"></script>
</head>
<body>
<h1>Random Data:</h1>
...
</body>
</html>
Note that this pulls jQuery from https://code.jquery.com
at runtime. To do this
without an active internet connection (i.e. for a robotics or IoT applications),
you can download jQuery and
serve it statically with Flask.
Making an Ajax Call
First, we need to add <script>
tags to add some Javascript to our page.
See the highlighted lines in the snippet below.
<!DOCTYPE html>
<html>
<head>
<title>Asynchronous Example</title>
<style>
*, table * { font-family: 'Courier New', monospace; }
</style>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"
integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4="
crossorigin="anonymous"></script>
</head>
<body>
<h1>Random Data:</h1>
<table>
{% for dp in data %}
<tr>
<td><b>Sensor {{loop.index}}: </b></td>
<td id="dp-{{loop.index}}">{{dp}}</td>
</tr>
{% endfor %}
</table>
<script type="text/javascript">
// Our Javascript goes here
</script>
</body>
</html>
Now we can start writing some Javascript inside the script
tags. First, we need
to add a function that will get our telemetry data. To do this, we'll use
jQuery's ajax
function.
function getTelemetry() {
$.ajax({
url: "/data",
success: handleTelemetryResult
});
}
The ajax()
function takes as input an object that tells jQuery what URL to call,
what request method to use (it uses GET by default), other data to send, and other options.
One of the options is also success
. This is given a function that will be executed when the Ajax
call is successful.
Similarly, there is also a failure
option to specify a function
to call if an error occurs with the request. We'll ignore that for now for the sake
of simplicity, but it would be a good practice to explicitly handle failures.
Updating the HTML
Our next step is to define that handleTelemetryResult
function. This function
receives the result (the array of data values) and should loop over each value
and update the data displayed on the screen. We do this using jQuery selectors
(which look something like $(...)
) and the .html()
function.
function handleTelemetryResult(result) {
for (let i = 1; i <= result.length; i++) {
$(`#dp-${i}`).html(result[i-1]);
}
}
This gets our data point HTML elements (#dp-${i}
) for each data point and sets
the inner HTML of that HTML element to that data point value from the result array.
Putting it All Together
Finally, we can put it all together and put all of our Javascript code inside
the script
tags in our HTML. We can call our code at a regular interval
using the Javascript setInterval
function.
// This function makes an AJAX call to get telemetry data
function getTelemetry() {
$.ajax({
url: "/data",
success: handleTelemetryResult
});
}
// This function takes the resulting telemetry data and updates the HTML
function handleTelemetryResult(result) {
for (let i = 1; i <= result.length; i++) {
$(`#dp-${i}`).html(result[i-1]);
}
}
// Run the getTelemetry function every 500 ms
setInterval(getTelemetry, 500);
This will now run our getTelemetry
function every 500 milliseconds to retrieve
data and update the UI.
Wrap-Up
There's a lot going on in the examples above and this just begins to scratch the surface of Javascript and asynchronicity, but with these concepts, you can now introduce some very dynamic behavior to a web application, update parts of the page dynamically without ever refreshing the page, or expand on these concepts a bit to send data, such as user input, back to the webserver.
The complete code for the examples above can be found here.