« Previous 1 2 3 Next »
Web applications with Julia
On the Rise
Developing a Notebook
You will be developing your notebook within the web interface provided by Pluto. The next step in starting up this interface, the connection between the web browser and the Julia process running on your computer, is to exit package mode in the REPL and, at the normal green julia> prompt, execute the commands:
using Pluto Pluto.run()
After some messages in the REPL and a short delay, a new window opens in your default web browser displaying the Pluto interface (Figure 2).
To replace this start page with an empty Pluto notebook, click the control that says Create a new notebook , where you'll begin the development of your web application. The first step, to remain organized, should be to click on the Save notebook text near the top of the page to give the notebook a name and a place in the filesystem. Now you can put any code in any of the notebook cells; rearrange the cells to present the notebook in a logical, pedagogically useful manner; and click on the little eye icons at the left of focused cells to decide whether your users will see their content or merely their results. If you haven't used Pluto or Jupyter (which follows similar conventions) before, it might be helpful at this point to consult one of the relevant articles or books linked elsewhere in this article; however, the interface and behavior is fairly intuitive.
If you have used Jupyter but have no experience with Pluto, you must be aware of one crucial difference: In Jupyter, cells are executed from the top down, but in Pluto, their order on the page has no effect on their execution. Cells in Pluto are executed in a logical order, following the graph of their dependencies. If you cause any cell to be executed, all the cells whose results depend on the outcome of that cell are automatically executed as well, following the chain of dependencies until everything is resolved. Therefore, you can order the cells to make the most presentational sense and even change your mind and the order later, without affecting the results. This freedom of cell ordering carries through to the web application that your notebook becomes: The user will be able to manipulate the controls in any order and will always see a reproducible, correct result.
You can use almost any package in the notebook by simply importing it with a using
statement in any cell; the consequent downloads and compilations happen automatically. The one required package for any notebook destined for PlutoSliderServer is PlutoUI
, which contains the code for the widgets with which users interact. In addition to the familiar slider, PlutoUI provides a large and useful collection of widgets [15] with which you can design a huge variety of interactive experiences.
Each widget performs the same function: It sets, or binds, the value of a variable to a value determined from the user's manipulation of the control. To insert a widget on the page, use the bind
macro, supplying it with a first argument naming the variable to bind and a second argument describing the widget to use. Every time the user manipulates one of these widgets, the value of its associated variable is (potentially) changed, and any cell that depends on the value of that variable is run (followed by all cells that depend on the result of that cell). This technique is thus ideal for demonstrations of such things as showing how the solution to an equation depends on the value of a parameter.
Figure 3 illustrates the use of the bind
macro to create widgets. The notebook first does the necessary import in the first cell (this would normally be hidden from end users of the application). The second cell uses Julia's macro syntax to invoke the macro to bind the value of a slider (which can take integer values from 1 to 10) to the variable x
. In the figure it's set at the value 6 with the slider. The third cell shows the use of the NumberField
macro, with a keyword argument supplying its default value. Note that in Pluto a cell's result is displayed above the cell content.
The final cell uses a Markdown "nonstandard string literal" to represent a Markdown string. The definition of this string literal is included neither in Julia nor in Pluto (nor in PlutoUI), but in another package that is automatically imported in all Pluto notebooks because the use of Markdown to create text cells is so common. The string has interpolated variables and uses Markdown syntax for italics and boldface. You can also use HTML if Markdown doesn't provide everything you need, either with an HTML nonstandard literal (html"<markup goes here>"
) or with the HTML()
function, which is able to incorporate interpolated variables that don't work in an HTML literal. An even more flexible way to use HTML in Pluto is with the HypertextLiteral
package [16], which I use extensively in my Ptolemy's Universe example.
When this notebook is made available over the Internet by PlutoSliderServer, the user will be able to use the slider and the input box to set the values of x
and y
, but not be able to change the code in the cell that computes their sum. Whether that code is visible or not is your choice, depending on your purpose for the notebook.
Setting Up the Server
You are going to serve a web page on the public Internet that allows visitors to run Julia programs on a remote server. Although the interactions through PlutoSliderServer are deliberately constrained, the possibility is always present that a user will find a way, either maliciously or inadvertently, to do something that is not part of your plans (e.g., the Julia process running on the server potentially has access to the filesystem). This software comes with no security guarantees; it's up to the developer to take measures to limit the scope of potential damage.
The best, and recommended, way of accomplishing this added security is to confine the Julia process on the server to some type of virtual environment, such as a container from which it is impossible to see the machine's root filesystem. The use of such a container makes the worst case scenario the destruction of the container itself; the server machine will be unaffected, and the administrator can start up the container again.
Among the several widely used container strategies is my favorite, which is built into most Linux systems: systemd-nspawn
. If you're using a Linux server and don't already have a favorite container solution of your own, I suggest learning how to use systemd-nspawn
to secure and isolate the server environment [17]. Another popular option you might already know about is Docker. It's not important what container technology you use, but it is important that you do something to isolate the Julia process on your server.
When merely testing out the notebook with the slider server, you can of course do everything on your local machine, where no container will be necessary.
After setting up your container and installing Julia on it, you should start a Julia REPL on the container, establish an environment as above (or simply use the default environment), and import the PlutoSliderServer package, which pulls in Pluto as a dependency.
The next step is to configure the web server on the host machine (not within the container). The following instructions will be specific to Apache, because that's where I tested my solutions. The configuration for Nginx will be quite similar.
The goal is to configure Apache to act as a gateway, or reverse proxy, to intercept normal HTTPS web requests to your public server and relay them to the PlutoSliderServer web server running in your container. Your visitors will not have to know anything about the slider server; they'll interact with your web application just as with any normal web page.
For Apache, first install or enable the required modules to enable proxying: the proxy
, proxy_html
, and proxy_http
modules. These might already be installed; check in the /etc/apache2/mods-enabled
directory. Next, add the lines in Listing 1 to the appropriate section of the configuration file for your virtual domain, if you have one, or for the default. For Apache, the files live in /etc/apache2/sites-enabled/
.
Listing 1
Apache Gateway Config
ProxyRequests Off AllowEncodedSlashes On ProxyPass "/sliders/" "http://127.0.0.1:2345/" nocanon ProxyPassReverse "/sliders/" "http://127.0.0.1:2345/" nocanon
The listing shows the proxy configuration for my slider pages, where I use /sliders/
in the URLs. Adjust those lines as needed for your server configuration. The directives pass requests from the user's browser to port 2345 on the local machine, which is the default port used by the PlutoSliderServer
package. You can change the port if you happen to be using that one for something else (see below). The 127.0.0.1 address refers to the local machine and is how you can talk to the slider server running on the container. Note that communication within the server machine is over http
– hence, in plain text. Although OK for now, you should set up Apache (Nginx, etc.) so that communication over the Internet uses https
.
The final step is to write some small scripts to start Julia and PlutoSliderServer in the container. You should be logged in as root within the container to complete this final step.
Just running Julia from the terminal won't quite work, because the process exits when you log out of the container. Even using nohup
is not a reliable way to keep Julia running. For this purpose, use the systemd-run
command, which, like systemd-nspawn
, is built in to most modern Linux distributions (at least those that use systemd, which, although far from beloved by all, has become a standard).
Specifically, create a file with the startup script:
#! /usr/bin/bash echo "Notebook server starting" julia echo "Done."
Name it (say) servenotebooks
, and make it executable with:
chmod 755 servenotebooks
As you can see, all the script does is start Julia. Invoking it with systemd-run
makes the Julia process persistent; that's its entire reason for existing.
One wrinkle might pop up that you will have to iron out, depending on the packages you use in your notebook. Some packages assume that Julia is being run on a computer with a graphical display (e.g., the X Window System); however, typically, this is not the case when running Julia on a server. It happens to be a problem with one of my example notebooks, which uses the Javis [18] package for creating animations. Javis will not even compile unless it detects a system for graphical output.
One solution to this problem, if you encounter it, is to install X Windows on the server. Another solution is to install the program xvfb-run
, which creates a virtual X Window environment for a program to run within. To use this solution, simply replace julia
in the startup script above with xvfb-run julia
.
When Julia starts, it runs the .julia/config/startup.jl
Julia script in your home directory. Because Julia will start in this container only for the purpose of running PlutoSliderServer, you can handle it in the startup script. Create (or edit, if it already exists) a file with the path mentioned here, providing it with the content:
using PlutoSliderServer run_directory( "/<the/notebook/directory>/"; Export_offer_binder=false)
The first line imports the only package needed to serve the notebooks; the package was added in a previous step. The second line starts the slider server in a mode where it monitors the directory in which you want to put your notebooks (replace /<the/notebook/directory>/
with this location). In this mode you can add, delete, or upload updated versions of all your notebooks, and the slider server will take note and serve the current state of the directory. You won't need to restart the server or take any other action.
The keyword argument (the bit after the semicolon) to run_directory
tells the server not to put a particular button on the notebooks you don't want. To see a list of all available options for this function, enter help mode in the Julia REPL by typing ? <function name>
.
With this step completed, your server is ready and running. You can check its status with the journalctl
command or kill it with the systemctl kill
command. Use the
julia --startup-file=no
command if you ever want to start Julia in your container without executing the startup script.
HTMX Web Applications
PlutoSliderServer is ideal if you already have a Pluto notebook that you want to turn into a web application or if the application you have in mind fits well with the PlutoUI set of widgets and the notebook framework.
In this section, I describe another way to create interactive web applications that use Julia on the back end for computations. With these methods, you don't need Pluto or any other Julia package aside from those used for your calculations and the HTTP [19] package, which provides functions for communication over a WebSocket.
The front-end interactions are designed around HTMX, which is a small JavaScript library that enhances HTML by allowing more elements to send messages to the server and by easily allowing fragments of the page to be replaced with the server's response, rather than having to reload the entire page on each request (similar to the AJAX method of request-and-response handling, but extended and made easier to use, with no explicit JavaScript required). This enhancement is exactly what's needed for a smooth interactive experience.
The method also uses the WebSocket extension to HTMX [20]. PlutoSliderServer uses WebSockets to make interactions snappier, although you didn't have to be aware of that when setting up your client and server. The method described in this section also uses WebSockets for the same reason.
Visit the sites under the HTMX Examples section in Table 1 for two examples of web applications I made with the methods from this section. These examples give you some idea of the kinds of things you can make, although your imagination is the only limitation.
You'll need to import the small HTMX library and its WebSocket extension into your HTML page. You can import directly from their source URLs provided in the documentation, but I prefer to download them to my server and load them locally to avoid potential problems, including when accessing the HTMX website. If the visitor can get to your page, then the HTMX library is guaranteed to load. The drawback is losing the potential advantage of the user's browser cache on first visit, but this is not a huge consideration because these JavaScript libraries are quite compact.
Put something like the following two lines near the top of the HTML page of your web application:
<script src="/htmx.js"></script> <script src="/ws.js"></script>
This example is for when you've placed the files at the root of your web directory, but of course you can put them elsewhere. The second line loads the WebSocket extension.
Because you won't have access to the PlutoUI collection of widgets, you'll need to have some familiarity with HTML to be effective with the method described here. Additionally, it's useful to know a little bit of plain JavaScript, in case you'd like to customize the user interactions beyond what HTMX and HTML can provide on their own (although you can go pretty far with these two technologies without the need for any JavaScript at all).
The next example illustrates the differences in defining widgets between PlutoUI and HTML. The previous examples created a slider in the Pluto notebook (Figure 3) with
@bind x Slider(1:10)
In HTML, the closest equivalent is:
<input type="range" name="s" min="1" max="10">
However, the two examples already show a big difference in behavior. The @bind
macro invocation in the Pluto notebook sets the value of x
directly when the user manipulates the slider, and any notebook cells that depend on the value of that variable are immediately executed (as well as all those further down in the chain of dependencies). All you need to do is write the programs that use x
and put in the @bind
macro for the slider anywhere it makes sense in your presentation.
In the HTML case, the <input>
tag merely puts a slider on the page. You have to do a bit more to make anything happen when the user slides the control. Listing 2 shows a fragment of an HTML page for a minimal slider that adds three attributes to the input tag for the slider (the tag is broken over three lines here).
Listing 2
Minimal Slider
<label> Pick a number <input data-hx-ext="ws" data-ws-send data-ws-connect="wss://<your/server/url>/" type="range" name="s" min="1" max="10"> </label> <p id='sr'></p>
All of these attributes invoke functions from the HTMX WebSocket extension (see the "Validation and the Data Prefix" box). The new attributes in Listing 2 specify, respectively, that the element (the slider) uses the extension, that it sends data over a websocket, and that it will send the data to the specified address. The WSS protocol is for encrypted WebSocket communication (analogous to HTTPS), which you should always use when communicating over the public Internet. For testing on your local machine, you can replace the address with
ws://127.0.0.1:PORT
Validation and the Data Prefix
The data
prefix in the three new attributes in the <label>
tag of Listing 2 is optional, in the sense that its omission will not affect the behavior of the client. It is there to make the page valid HTML; custom attributes such as hx-ext
are not part of HTML, so validators will complain; however, they will ignore any attribute beginning with data-
. Allowing the data
prefix is a feature of HTMX. Note that PlutoSliderServer pages do not
pass validation, and their sources cannot be usefully inspected by a curious user, which is a disadvantage. Pages made with HTMX can be valid HTML, and their code is legible with view source
.
and use the port number that you've set up in place of PORT
. Finally, the element is wrapped in a label, which associates the instruction with the slider. Following the labeled slider is an empty paragraph with an id
that is there to receive the response from the server.
Every time the user changes the position of the slider, a value associated with name s is packaged into a message and transmitted over the open WebSocket to Julia running on the server. The client sends the message after the user stops interacting with the control (e.g., by moving the mouse away), so you needn't worry about a massive cascade of messages being transmitted while the slider is in motion.
Listing 3 shows a minimal working Julia program that defines a server that can respond to the slider in the previous example. To begin, create an environment for your Julia project in the REPL's package mode, as before, and add the HTTP and JSON packages.
Listing 3
Minimal Server Example
01 using HTTP, JSON 02 const PORT = 8660 03 04 function startserver() 05 WebSockets.listen("127.0.0.1", PORT) do ws 06 for msg in ws 07 d = Meta.parse(JSON.parse(msg)["s"]) 08 WebSockets.send(ws, """<p data-hx-swap-oob='true' id='sr'> You picked $d </p>""") 09 end 10 end 11 end
The JSON package is here for one function: JSON.parse()
, which extracts variables from the messages sent by the client. The information returned by the server is always in the form of HTML fragments, not JSON: This is the philosophy behind HTMX.
Variables extracted from browser messages are always strings. In this example, the string is passed to Meta.parse()
, part of Julia Base, which converts strings to Julia expressions; in this case, it's used to convert the string to an Int
.
The HTTP package provides the two communication functions around which you will build your applications. WebSockets.listen()
takes a second argument giving the IP number on which to listen, and a third argument for the listening port. This function opens a WebSocket connection on the given address and port. The address shown in the listing refers to the local machine, either your development computer or the server container on which you'll eventually deploy.
You'll notice when you run this or similar code that the REPL blocks with a message that Julia is listening on the WebSocket. If you'd prefer to continue to work in the REPL while this program is running, perhaps to start further servers, the HTTP package provides another, non-blocking version of the function. It's the same, but using an exclamation mark; the call becomes:
WebSockets.listen!("127.0.0.1", PORT)
In the program you run on the server with the systemd-run
command, you can start up any number of listening WebSocket servers with the non-blocking version of the call, but the final one should use the blocking version to keep the program alive and listening for connections.
The first argument to WebSockets.listen()
is a handler function that takes a single Websocket
argument; it's provided here as an anonymous function constructed with Julia's do
keyword. This function is the one that does things with the incoming messages, so it should include the command that sends the result back to the client, which is accomplished by the WebSockets.send()
call. The for
loop keeps the connection open until the user closes it, which normally happens on closing the web page. Listing 3 shows the typical pattern for using the HTTP package to communicate over WebSockets and is the pattern I use in the more complicated examples listed under HTMX Examples in Table 1.
In more detail, the function startserver()
in Listing 3:
- Begins to listen for WebSocket messages on the local machine coming through
PORT
- Parses each message received, extracts the value associated with the name
s
, and assigns that to the variabled
- Immediately sends a message back to the client consisting of an HTML fragment with the value assigned to
d
interpolated
The example returned by the HTML fragment has a paragraph element with two attributes. The first, hx-swap-oob
(with the optional data
prefix to help keep the HTML standards compliant), tells HTMX that the fragment is to be inserted out of band and should replace the existing element with id='sr'
. In the context of HTMX, "out of band" means that the element to be replaced is not the same as the element that sent the message: The slider itself is not to be replaced, but a different element – in this case, a paragraph.
Referring back to Listing 2, notice the existing empty paragraph with id='sr'
. It's there as a placeholder, ready to be replaced after the user manipulates the slider. Figure 4 shows what this minimal example looks like in the browser after the user has placed the slider in a position corresponding to the number 6. The empty paragraph has been replaced by that constructed in Listing 3, containing the text You picked 6
.
Listings 2 and 3 illustrate the mechanics of using the JavaScript HTMX library and the Julia HTTP package together to build an interactive page backed by Julia computations on a server. By using this method with a little HTML and JavaScript knowledge, you can create any application you can imagine. Often in these applications you will want to insert something other than text into the page: perhaps images, video, or sound. One obvious way to accomplish this is to have your Julia program save the media that it generates in a file on the server and send an element to the page that loads the file. This is the usual way that, for example, an image tag works, referring to the URL of the image to be loaded in its src
attribute.
This reasonable approach has at least two minor disadvantages: It requires an extra request to be sent by the client, which adds an interaction delay, and it litters your server with media files that you will need to clean up. If you delete them too soon, your users' pages might wind up trying to load resources that no longer exist, and if you leave them around too long, they will eat into the space on your server.
An elegant solution to these problems is to exploit an underutilized feature built in to HTML: the data
URL [21]. With this feature you can send any kind of media a web browser can support (e.g., images, sounds, etc.) directly down the wire as part of the server's response, rather than sending the URL of a file that the browser subsequently has to request. No files need be created or stored. The media becomes part of the page, just as its text content is, rather than a separate resource.
To use this feature in your web applications, you'll need a way to encode the binary media data as text. Among the myriad ways to do this, the standard in browsers is Base64 encoding [22], provided in Julia by the IBase64 package.
Listing 4 shows the server program from Listing 3 with a few lines added that create a plot from user input on the slider, encode it, and send it to the client for insertion on the page. The program imports the Plots
and Base64
packages to create and encode the graphics. At the beginning of startserver()
an input-output buffer is assigned to the variable io
. You'll need this buffer object as part of the conversion from a binary stream to a block of text.
Listing 4
Server with Graphics
01 using HTTP, JSON, Plots, Base64 02 const PORT = 8660 03 04 function startserver() 05 io = IOBuffer(); 06 x = 0.0:2PI/1000:2PI 07 WebSockets.listen("127.0.0.1", PORT) do ws 08 for msg in ws 09 d = Meta.parse(JSON.parse(msg)["s"]) 10 @info d 11 @info typeof(d) 12 WebSockets.send(ws, """<p hx-swap-oob='true' id='sr'>You picked $d</p>""") 13 p = plot(x, sin.(d .* x); label="sin($(d)x)") 14 show(io, MIME"image/png"(), p) 15 data = base64encode(take!(io)) 16 WebSockets.send(ws, """<img data-hx-swap-oob='true' id='plot' src='data:image/png;base64,$data' alt='sin(1/x)'>""") 17 end 18 end 19 end
Within the for
loop you can see four new lines. First, the program creates a simple plot of the sin()
function that incorporates the value selected by the user, assigning this plot to the variable p
. The next two lines read the plot data into io
as a PNG image, convert it to text (Base64-encoded), and store it in the variable data
.
The final line in the for
loop has an additional send()
call that sends the plot to the client, replacing the existing image element (identified by its id
) with an image element that embeds the plot in its src
attribute. As you can see in the example, you have to tell the browser the mime type of the data and mention that it uses base64
encoding.
Listing 4 also illustrates that you are not limited to sending one response for each message received but can send any number of them, targeting any set of elements on the page.
The client code for this example would be the same as in Listing 2, with the addition of the line
<img alt='' src='' id='plot'>
to create a placeholder element for the plot.
Figure 5 shows a screenshot of the browser after the user has manipulated the slider to select the number 8. If you implement the client and server from Listings 3 and 4 and play with the slider in your browser, you'll see that the plot updates quickly, providing a good interactive experience, especially if you run the server on the same machine as your browser. Naturally, if the server and client are communicating over the Internet, the experience will be affected by the speed of the network, and the response will not always be smoothly interactive, which is an unavoidable drawback of all applications that depend on calculations carried out on a remote server. The principal way to minimize such issues is to avoid sending massive amounts of data in response to user actions. If you're sending an image, for example, take advantage of compressed file formats (e.g., JPEG) and limit the resolution to reduce the quantity of data as much as possible.
The other source of delays in interactivity is the time needed to carry out the calculations on the server. In this case, Julia's speed helps and is one reason it's a good choice for these applications: For the typical calculations supporting the types of pedagogical purposes that my examples serve, the calculations are sub-second, and calculation time is not a major factor limiting the experience of interactivity.
You can implement the example in this section with the PlutoSliderServer method, as well; which one you might choose depends on your plans for further development, whether you enjoy working with Pluto notebooks, and whether having an HTML page that can pass validation is important to you. For an example of an application that uses the techniques described in this section but that would be cumbersome to build with PlutoSliderServer, see the vortex dynamics example in Table 1. You can examine the source of the client in the usual way, and the page explains how to obtain the source code for the server component.
« Previous 1 2 3 Next »
Buy this article as PDF
(incl. VAT)
Buy ADMIN Magazine
Subscribe to our ADMIN Newsletters
Subscribe to our Linux Newsletters
Find Linux and Open Source Jobs
Most Popular
Support Our Work
ADMIN content is made possible with support from readers like you. Please consider contributing when you've found an article to be beneficial.
