"Bruised" is a little how I feel about my first experience porting a program to
ClojureScript — configuring production builds has a few traps for newcomers.
That said, I am very pleased with the results and excited to do more working with ClojureScript.
My Dad is a soil conservation officer with DELWP. Many years ago I wrote a small Java applet for use in his workshops. It help farmers estimate how much water can flow down an earthen channel without causing soil erosion. Here's a screenshot of the original Java version:
Despite its simplicity, the channel flow calculator has been a useful and popular tool. The only drawback has been the inconsistent support for Java applets in web browsers; particularly in government departments. To solve this, we recently decided to rewrite it in HTML and JavaScript. It's also a chance to indulge my interest in ClojureScript!
Why ClojureScript?
Well, less JavaScript for a start. Rather than trying to incrementally fix
JavaScript's lack of modules, non-existent standard library, unexpected type
coercion rules and inconsistent browser support, ClojureScript starts afresh
with solid language fundamentals and good design choices based the Clojure Lisp
dialect.
ClojureScript compiles down to lowest-common-denominator JavaScript, meaning
that my code will run consistently on any modern browser. I can confidently use
the full breadth of the elegant language features, powerful data-types and the
convenient standard library. I get real module support. Last but not least,
thanks to the optimising compiler, I can fearlessly pull in large libraries,
knowing that my visitors will only download the code they need.
ClojureScript trap 1: Unwanted :main
The basic translation from Java to ClojureScript was straightforward; even the
Java and JavaScript canvas-drawing APIs are nearly identical. After some messing
around, I even managed to get Lein, Clojure's de-facto build tool,
auto-rebuilding and live-updating my web browser (via plugins lein-cljsbuild
and lein-figwheel). The trouble came when I needed to produce the optimised
production version for publication.
ClosureScript utilises the Google Closure compiler to remove unused JavaScript code, turning around 1MB into 90kB (20kB Gzipped). All this by simply setting :optimizations :advanced
in the build settings.
The first trap was that the :main
setting used in development builds can't be left in place when you switch on :optimizations :advanced
. Not that Clojure gives up this information willingly. Here's my faulty build script:
;; build.clj
(require 'cljs.build.api)
(cljs.build.api/build "src"
{:main 'channel_flow.core
:output-to "out/main.js"
:optimizations :advanced})
(System/exit 0)
And here's the output:
$ java -cp cljs-1.8.51 clojure.main build.clj
Exception in thread "main" java.lang.AssertionError: Assert failed: No file for namespace channel_flow.core exists
Not so helpful. That error kept me busy for nearly two hours, including diving
into the ClojureScript source code to figure out what was going on. After
removing the :main
setting, the error disappears. It's worth noting that if
you're building through lein-cljsbuild, it does the right thing and ignores
the unnecessary :main
setting.
ClojureScript trap 2: Symbol renaming
The second trap was that after switching from :optimizations :simple
to :optimizations :advanced
, I was seeing JavaScript errors like document.ga is undefined
. It's a bizarre feeling when your code was working only moments ago. Optimisation isn't meant to change the behaviour of code, right?
Turns out it does, if you're not abiding by certain rules. I was using the following HTML form:
<form name="mannings">
<label>Top width: <input type="number" name="w1" /></label>
…
I was referencing this in ClojureScript to trigger the recalculation when inputs were changed:
;; Don't do this
(.addEventListener document.mannings "input" redraw)
The Google Closure compiler was helpfully renaming document.mannings
to document.ga
to make my code shorter; thereby breaking the reference to the HTML form:
// "Optimised" (broken) JavaScript
document.ga.addEventListener("input",Oe);
All this makes sense in hindsight; I just didn't expect that enabling
compilation optimisation do anything other than slow down the build and produce
a smaller output file.
The solution is to explicitly advise the Google Closure compiler that
document.mannings
is something external to this file and not be renamed. Create an externs.js
file that references the variables:
// externs.js
document.mannings = {
w1: null,
w2: null,
cd: null,
s: null,
m: null,
fd: null,
v: null,
q: null
};
Declare the externs to the compiler in your build script:
:optimizations :advanced
:externs ["externs.js"]
The results
After clearing these initial hurdles, the updated
channel flow calculator is now
working well (picture below). A few weeks on, now that my frustration has faded,
I'm actually really looking forward to my next ClojureScript project.