A Clojure HTML Server Live Reload Template
I made a template that you can use to generate code for a minimal clojure HTML server with a "live reload" development experience.
Update: Subsequent to this writing I found that the ring-refresh library accomplishes the same goal as the following post and has existed in library form since 2012.
Live Reload + ClojureScript
The Clojure programming language and ecosystem prides itself for being in the vanguard of the effort towards "live programming". Clojure developers are encouraged to start every programming session by running a REPL and then building up and redefining their program state piecemeal.
Furthermore the ClojureScript community was early to embrace live-reloading for browser applications. Bruce Hauman created Figwheel to ameliorate the pains of developing a single page app. When you modify and save your ClojureScript code, Figwheel reloads your application in the open browser tab without requiring you to tab or mouse over to the browser and manually reload the page, wiping out your application state. Additionally Figwheel streamed changes to CSS files to the open browser tab without requiring manual reloading. Soon after the live reload workflow was introduced it became the default way of working on ClojureScript applications, using Figwheel or later on shadow-cljs, which offered similar functionality.
Without ClojureScript
Having gotten used to the live reload workflow, I recently became frustrated when I realized that I didn't know how to produce an equivalent feedback loop when ClojureScript was not in the picture. I wanted to build an Clojure application that served HTML and CSS and I wanted my browser tab to update automatically whenever I re-evaluated the clojure code that produced the HTML or saved any CSS file.
http-kit and Websockets
My plan was to use Websockets to notify my browser to reload itself. The http-kit clojure library has support for communicating via both HTTP and websockets (surprisingly as of this post's publication the example in the official docs uses a macro that is now deprecated)
I set up a reitit router with a /live-reload
route backed by a handler that will register the websocket connection in a clojure atom, *channels
. The notify!
function can then be called at any time to send a reload message to any open connections.
(require '[org.httpkit.server :as http-kit])
(require '[reitit.ring :as reitit.ring])
(defonce *channels (atom #{}))
(defn ws-handler
[request]
(http-kit/as-channel request
{:on-open (fn [channel]
(swap! *channels conj channel))
:on-close (fn [channel _status]
(swap! *channels disj channel))}))
(defn notify!
[& _args]
(doseq [ch @*channels]
(http-kit/send! ch "reload")))
(def router
(reitit.ring/router
[["/live-reload" {:get ws-handler}]]))
Next I served a Hiccup-based HTML page at the root route.
(require '[ring.middleware.resource :as ring.resource])
(require '[myapp.views.home :as home])
(defn- html-response [html-string]
{:status 200
:headers {"Content-Type" "text/html"}
:body html-string})
(def router
(reitit.ring/router
[["/" {:get (fn [_req] (html-response (home/page)))}]
["/live-reload" {:get ws-handler}]]))
(def handler
(-> (reitit.ring/ring-handler router)
(ring.resource/wrap-resource "public")))
(http-kit/run-server #'handler {:port 3000 :join? true})
With the ring.resource
middleware in place I can add static assets like JavaScript or CSS files to my resources/public
directory and they will be served by my server. I created a short JavaScript file that setups up a websocket connection when it is loaded and will trigger a page reload on any websocket message from the server.
// resources/public/js/live-reload.js
const socket = new WebSocket("ws://localhost:3000/live-reload");
// Listen for messages
socket.addEventListener("message", (event) => {
console.log("Message from server ", event.data);
location.reload();
});
Next, I needed to link the live-reload JavaScript into my HTML page in the namespace that creates this page. Note that I moved the notify!
function to a new namespace to avoid a circular namespace dependency.
(ns myapp.views.home
(:require
[hiccup2.core :as h]
[myapp.reload :as reload]))
(defn wrap-html
[body]
(str
(h/html
[:html
[:head
[:script {:src "js/live-reload.js"}]]
body])))
(defn page
[]
(wrap-html
[:body
[:p "Hello world"]]))
(reload/notify!)
This was all the code I needed to get a live-reload HTML page. I can edit the body, re-evaluate the whole namespace with one quick keystroke in my Emacs buffer connected via CIDER to the running Clojure REPL, and my page updates automatically due to the call at the end of the namespace to (reload/notify!)
.
Reloading CSS
It was easy enough for me to add a CSS file to my project, but after I saved the file I was back to tabbing over to my browser to reload the page manually.
# resources/public/css/styles.css
color: blue;
Figwheel and shadow-cljs have built-in support for watching directories for changes to files and performing a reload. A web search yielded the Beholder library by Nextjournal, a stand alone Clojure directory watcher library. Once this watcher is started every change to a file in `resources` will trigger the reload event in my open browser tab.
(require '[nextjournal.beholder :as beholder])
(defonce *file-watcher (atom nil))
(defn start!
[]
(swap! *file-watcher
#(or %
(beholder/watch (reload/notify!) "resources"))))
Bundling Into a Template
This ended up not being very much code on top of the libraries used, certainly not enough to justify creating a library of its own out of it. However, I was put off by the idea of copying and pasting this code every time I wanted to spin up a project with this functionality.
I decided to package this example into a template which you can use with the deps-new tool. First make sure you've installed deps new according to the instructions in the README. Then you can run one terminal command to output a codebase in this shape, replacing yourcorp/app-name
with your desired application name.
$ clojure -Sdeps '{:deps {io.github.adamfrey/clojure-html-server-live-reload-template {:git/sha "4bc908f5d601e2ba24c477f7bc123e83f89635a3"}}}' -Tnew create :template afrey/html_server_live_reload_template :name yourcorp/app-name
Consider checking the Github respository for a more recent value for :git/sha
.
After running that command check the README
in the created directory for additional instructions.
Production Ready?
So far this template has satisfied my personal goal of being able to quickly spin up clojure HTML projects with a delightful live reload feedback loop. Is it useful beyond that? I'm not sure. Growing up the generated application into a production application would likely involve many changes. Most obviously a state management tool like Component or any of the similar tools in that space would be a good upgrade from the top level defonce
'd atoms from my code. But I wouldn't want be presumptuous and make any of those decisions for anyone else.
September 1, 2023