Pattern for smooth SPA development with SWI-Prolog
The gist of this pattern is to use SWI-Prolog to serve your single-page-application (SPA). Then, when your application requests JavaScript that it doesn’t have, it behaves like a proxy, passing the request onto the SPA development server and returning the response.
I use this with ClojureScript and Logtalk with SWI-Prolog on the backend, but there’s no reason it shouldn’t work with other SPAs (I’ve also used VueJS) and without Logtalk. If Web Development with SWI-Prolog is new to you, you might want to run through Annie’s fantastic tutorial first.
Step 1: Use SWI-Prolog to Serve the App Page
SPA applications work with an index.html
page you’ll find somewhere in your
SPA directory. Inside that index.html
, there will be a <div/>
or some such tag
where the application will appear, as well as some scripts that make the magic
happen. You can either copy this file across to your SWI-Prolog directory and
have SWI-Prolog serve it, or recreate it with termerized HTML.
SPA applications work with an index.html
page you’ll find somewhere in your
SPA directory. In that index.html
, there will be a <div/>
or some such tag
where the application will appear, as well as some scripts that make the magic
happen.
You can either copy this file across to your SWI-Prolog directory and have SWI-Prolog serve it, or recreate it with termerized HTML (a handy Prolog trick for building pages dynamically). Usually, I opt for termerized HTML so I can keep things consistent across webpages—like having the same navbar—and get dynamic behaviour, such as only showing a “Logout” button if a user is logged in.
Here’s how I set up the app page for ClojureScript:
:- handler('/myapp/', app_page, [id(myapp)]).
app_page(_Request) :-
reply_html_page(
[title(myapp)],
[
div(id='app', [p('Loading ...')]),
script([src='js/compiled/myapp.js', type='text/javascript'], []),
script(type='text/javascript', 'myapp.core._main();')
]).
Step 2: Getting that JavaScript
Now that the page is served, let’s tackle the JavaScript! For my ClojureScript
application to run, I’m requesting some JavaScript at js/compiled/myapp.js
.
This in turn, triggers requests for a far-too-many-to-bother-counting number of
JavaScript files. None of these files are visible to my SWI-Prolog application.
Also, none of these files are used in production: by then they’re all compiled
down to one “little” file.
Why Proxy?
So, what I want SWI-Prolog to do is play middleman: when it’s asked for these
files, it should fetch them from whatever’s serving my SPA. This means I need
my SPA’s usual development server running too. For ClojureScript, that’s lein figwheel
(a nifty ClojureScript tool), serving on http://localhost:3449
.
You’ll need to tweak the code below for your own SPA.
I get SWI-Prolog to serves these files by combining the prefix
we use for
serving static files with SWI-Prolog’s HTTP Client libraries. In my app the
requests to the scripts are relative to the URL being served, so it’s actually
going to ask for /myapp/js/compiled/myapp.js
, which is why the handler here
is root(myapp)
. If yours is an absolute URL, adapt accordingly.
Here’s the proxy magic in action:
:- use_module(library(http_client), [ http_get/3 ]).
:- use_module(library(http_open), [ http_close_keep_alive/1 ]).
:- use_module(library(http_header), [ http_reply_header/3 ]).
:- handler(root(myapp), spa_proxy, [id(spa_proxy), prefix, priority(-1)]).
handle(Request) :-
proxy_url(Request, Url),
proxy(Url).
proxy_url(Request, Url) :-
member(path_info(RelPath), Request) ; RelPath = '/'), !,
% comment these writes and nl out when you've seen it working!
write(user_output, 'Forwarding: '),
write(user_output, RelPath),
nl(user_output),
% adapt the base url to your spa
atom_concat('http://localhost:3449', RelPath, URL).
proxy(Url) :-
http_get(Url, Data, [
headers(Headers),
to(string),
connection('Keep-alive')]),
http_close_keep_alive(URL),
member(content_type(ContentType), Headers),
write('Content-type: '), write(ContentType),
nl,
nl,
write(Data).
Step 3: Enjoy!
That should be about all you need. Spin up both processes, navigate to your SPA app served by SWI-Prolog, and watch the magic happen.
When it comes to production deployment just tweak the myapp
handler to return
HTML that loads your compiled JavaScript instead of the development stuff, and
skip loading the spa_proxy
handler. I do this with Logtalk, so for me this is
just commenting out one line in the loader.lgt
file, but you can do the same
with SWI-Prolog if you use some kind of loader file too.
This was a bit of a pain to figure out the first time, but it has saved me so much bother and made developing frontend applications on a Prolog backend quite practical. I hope it helps you out too!