Pattern for smooth SPA development with SWI-Prolog
The gist of this pattern is to use SWI-Prolog to serve your application page. 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 SPA’s (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. 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.
Usually I opt for termerized HTML so I can use it for consistency across webpages, such as having the same navbar. I also use termerized HTML so I can get dynamic behaviour, such as only rendering a “Logout” button if a user is logged in. So to achieve Step 1 here for ClojureScript, I’d have something like this:
:- 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
For my ClojureScript application to run, I’m requesting some JavaScript at
js/compiled/myapp.js
. In turn, this triggers requests for a
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.
So what I want SWI-Prolog to do when it’s asked for these files is to ask
whatever is serving my SPA for them. This means I need whatever process is
used to typically serve my SPA also running during development, for
ClojureScript that’s lein figwheel
. That serves on http://localhost:3449
,
you’ll need to adapt the code below for your 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.
:- 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 change the myapp
handler to return HTML that loads
your compiled JavaScript instead of the development JavaScript, and don’t load
the spa_proxy
handler. I do this with Logtalk, so for me this is just
commenting out the a file in the loader.lgt
, but you can do the same with
SWI-Prolog if you also use some kind of loader file.
This was a bit of a pain to figure out the first time, but has saved me so much bother and made developing frontend applications on a Prolog backend quite practical. I hope it helps you out too!