Improving User Experience with Server-Sent Events
5 min read
In the last two posts, we have built a little app that shows the current location and displays the nearest ice cream shops:
The app gets the location data from the GPS module and the ice cream locations via the OpenStreetMap API. This means that there are a lot of external factors which can cause our app to slow down or even stop working. Now, in order to keep the user happy, let's add at least some small feedback which helps the user to understand what is currently going on.
In order to trigger this feedback we will use Server-Sent Events.
What are Server-Sent Events?
In a typical website, the client (for example the front-end of our app) requests the server for content and receives data as response. However, there might be cases when the server needs to push updated data to the client without the client asking for it. For this purpose, server-sent events have been standardized as part of HTML5 by the W3C in 2008. It is supported by all major browsers.
Basically, server-sent events establish a uni-directional connection from the server to the client. It allows to update a specific DOM component without having to reload the whole page. In PicoLisp, we can implement Server-Sent Events with just a couple of lines of code.
Specifying the DOM component in the front end
In the first step, let's define the front end component that should update itself with help of server-sent events. In case of our app, we choose to add some text below the app title. For example, while the request to the OSM API is running, we want to display the text "Fetching Ice Cafes...":
By displaying this text, the user knows that our app is still waiting for data.
First of all, let's define the DOM element that should receive dynamic updates.
(<h6> '((id . "icecream") "red")) )
With this we define a h6-element with the id "icecream" and the CSS class "red" (which simply prints red text as defined in the pre-installed
lib.css) using in-line CSS styling. (Read here for more on in-style CSS).
Now we connect this element to receive server-sent event updates, by using the
(serverSentEvent "id" 'var . prg)
The first argument should be the ID of a DOM component in the page. (...) The ID should be unique within this program session for all
serverSentEventuse cases, as it is also used as a key to an internal association table.
The second argument
varis a variable which is automatically bound to a socket as soon as the client establishes the event stream connection, i.e. shortly after the page with the embedded call to serverSentEvent is displayed. In essence, this socket is then connected to the DOM component "id".
When that happened, the
prgbody is executed to set up everything needed for further event sending via the socket in
varis automatically set to
NIL, and the socket closed, when the client closes the connection. Then the server should clean up whatever is necessary.
Basically this means that we need to define a global variable for the
var parameter which is used to establish the connection. Let's create a new global called
*Sse and add it to our globals list:
(local) (*Sse *IceCreamOsm *Latitude *Longitude *IceList *SupermarketList)
Now we can call
serverSentEvents somewhere on our page. The position is not really relevant, but it might sense to keep it close to the connected DOM element to keep the code readable. Since we just want to pass-through some simple status text, we do not need to define any
prg. So all we need is to add this line:
(serverSentEvent "icecream" '*Sse)
Defining the server side
The counter part to
serverSend. As the name already says,
serverSend defines when and which data is sent to the client.
serverSendmay be called anytime while the connection is open:
(serverSend 'sock . prg)
It sends all output (HTML text) generated by
prgto the inner HTML of the component connected to the socket sock.
Let's use it to print out error messages if GPS is not enabled or available, and otherwise the GPS data of the current location.
task function is checking the position every 6 seconds. Within this check, let's generate a string that is sent to the
*Sse socket linked to the
icecream component. This string is then pushed to the client and displayed without updating anything else.
(task -6000 1000 (nond ((location?) (serverSend *Sse ,"Please enable \"Location\"") ) ((gps) (serverSend *Sse ,"Waiting for location") ) (NIL (setq *Latitude (car @) *Longitude (cdr @)) (serverSend *Sse (prin (lat *Latitude) ", " (lon *Longitude))) ) ) ) )
Note: The comma
, is a read-macro in case you want to add multi-language support. If you only use one language, it is not needed.
Likewise, we can also add a server-sent update each time any of the buttons is pressed. Since the button press calls a function
icecream (defined in
lib.l), we can call
(de iceCream (Msg Osm Category Type) (serverSend *Sse Msg) (ssl "overpass-api.de" ...
Msg is specified in the button press:
(gui '(+Style +Button) "button-icon bg-white mb-5 mx-3" T "icecream/img/supermarket.png" '(setq *SupermarketList (iceCream ,"Fetching Supermarkets ..." *IceCreamOsm "shop" "supermarket")) ) )
With these small modifications, we receive a much more user-friendly app that gives a minimum of feedback of what is internally going on.
The source code for this example can be found here and this is the zip file for PilBox.