In this tutorial we are going to investigate the issue we met in the previous tutorial and try to solve it.
If you want to start working from the end of the previous tutorial, assuming you've git installed, do as follows.
git clone https://github.com/magomimmo/modern-cljs.git
cd modern-cljs
git checkout tutorial-05
git checkout -b tutorial-06-step-1
Our latest tutorial ended with a not so nice issue. Just to recap we did as follows:
- created the
login.html
page and the correspondinglogin.cljs
source file; - created the
shopping.html
page and the correspondingshopping.cljs
source file; - launched the app in the usual way
lein ring server # from the project home directory
lein cljsbuild auto # from the project home directory in a new terminal
lein trampoline cljsbuild repl-listen # from the project home directory in a new terminal
We then visited the shopping page in the browser and discovered
that the init
function we set for the onload
property of the JS
window
object was not the one we defined in shopping.cljs
, but the
one we defined in login.cljs
.
As we anticipated in the previous tutorial, this behaviour
depends on the Google Closure Compiler driven by the lein-cljsbuild
plugin.
In the first tutorial, we set :cljsbuild
in
project.clj
to configure the Google Closure Compiler with the following
options:
(defproject ....
...
:cljsbuild {:builds
[{:source-paths ["src/cljs"]
:compiler {:output-to "resources/public/js/modern.js"
:optimizations :whitespace
:pretty-print true}}]})
The :source-paths
option instructs GCSL to look for any CLJS source
code in the src/cljs
directory structure. The :output-to
option of
the :compiler
keyword instructs GCSL to save the compilation result
in resources/public/js/modern.js
.
I'm not going to explain every single detail of the CLJS/GCSL pair of
compilers. The only detail that is useful for investigating and
eventually solving the above issue is that the pair of
compilers generates a single JS file
(i.e., resources/public/js/modern.js
) from all of the CLJS files
it finds in the src/cljs
directory and subdirectories
(i.e., modern.cljs
, connect.cljs
, login.cljs
, and shopping.cljs
).
Both login.cljs
and shopping.cljs
had a final call to (set! (.-onload js/window) init)
, which is therefore called twice: once from
login.cljs
and once from shopping.cljs
. The order of these calls
is critical, because whichever comes first, the other is going to
mutate its previous value: a clear case against JS mutable data
structures?
From the above discussion the reader could infer that that CLJS is good only
for a single page browser application. Indeed, there is a very modest solution
to the above conflict between more calls setting the same onload
property of
the JS window
object: code duplication!
You have to duplicate the directory structure and the corresponding build options for each html page that is going to include the single generated JS file.
Here are the bash commands you should enter in the terminal.
mkdir -p src/cljs/{login/modern_cljs,shopping/modern_cljs}
mv src/cljs/modern_cljs/login.cljs src/cljs/login/modern_cljs/
mv src/cljs/modern_cljs/shopping.cljs src/cljs/shopping/modern_cljs/
cp src/cljs/modern_cljs/connect.cljs src/cljs/login/modern_cljs/
cp src/cljs/modern_cljs/connect.cljs src/cljs/shopping/modern_cljs/
rm -rf src/cljs/modern_cljs
And here is the modified fragment of project.clj
(defproject ...
...
:cljsbuild
{:builds
;; login.js build
{:login
{:source-paths ["src/cljs/login"]
:compiler
{:output-to "resources/public/js/login.js"
:optimizations :whitespace
:pretty-print true}}
;; shopping.js build
:shopping
{:source-paths ["src/cljs/shopping"]
:compiler
{:output-to "resources/public/js/shopping.js"
:optimizations :whitespace
:pretty-print true}}}})
NOTE 1: To understand the details of the
:cljsbuild
configurations, I strongly recommend you read the advanced project.clj example from the lein-cljsbuild plugin.
Finally you have to include the right JS file (i.e., js/login.js
and js/shopping.js
) in the script tag of each html page
(i.e., login.html
and shopping.html
).
Most would call the above solution a kind of incidental complexity. What's worse is the fact that each emitted JS file, no matter how smart the GCSL compiler is in reducing the total size, is different from the others--there is no way for the browser to cache the first file downloaded and serve the others from cache.
Now the simple made easy way:
- remove the call
(set! (.-onload js/window) init)
from bothlogin.cljs
andshopping.cljs
files; - add the
:export
tag (metadata) to theinit
function in bothlogin.cljs
andshopping.cljs
files; - add a
script
tag calling the correpondinginit
function in bothlogin.html
andshopping.html
files; - you're done.
NOTE 2: If you do not
^:export
a CLJS function, it will be subject to Google Closure Compiler:optimizations
strategies. When set to:simple
optimizations, the GCSL compiler will minify the emitted JS file and any local variable or function name will be shortened/obfuscated and won't be available from external JS code. If a variable or function name is annotated with:export
metadata, its name will be preserved and can be called by standard JS code. In our example the two functions will be available as:modern_cljs.login.init()
andmodern_cljs.shopping.init()
.
Here is the related fragment of login.cljs
;; the rest as before
(defn ^:export init []
(if (and js/document
(.-getElementById js/document))
;; get loginForm by element id and set its onsubmit property to
;; validate-form function
(let [login-form (.getElementById js/document "loginForm")]
(set! (.-onsubmit login-form) validate-form))))
;; (set! (.-onload js/window) init)
And here is the related fragment of shopping.cljs
;; the rest as before
(defn ^:export init []
(if (and js/document
(.-getElementById js/document))
(let [the-form (.getElementById js/document "shoppingForm")]
(set! (.-onsubmit the-form) calculate))))
;; (set! (.-onload js/window) init)
Here is the related fragment of login.html
<script src="js/modern.js"></script>
<script>modern_cljs.login.init();</script>
And here is the related fragment of shopping.html
<script src="js/modern.js"></script>
<script>modern_cljs.shopping.init();</script>
You can now run everything as usual:
lein ring server # from the project home directory
lein cljsbuild auto # from the project home directory in a new terminal
lein trampoline cljsbuild repl-listen # from the project home directory in a new terminal
and visit the http://localhost:3000/shopping.html to activate the
brepl connection to verify that now you can serve more pages with the
same modern.js
generated JS file.
ATTENTION NOTE: The brepl connection created via
lein trampoline cljsbuild repl-lissen
is a little bit unstable. You could need to reload more times thelogin
or theshopping
URL to activate/reactivate the brepl connection.
If you created a new git branch as suggested in the preamble of this tutorial, I suggest you commit the changes as follows
git commit -am "Simple made easy."
In a subsequent tutorial we'll introduce domina event management to further improve our functional style in porting Modern JavaScript samples to CLJS.
Next step - Tutorial 7: Compilation Modes
In the next tutorial we're going to explore CLJS/GCSL compilation modes by
using the usual lein-cljsbuild
plugin of leiningen
.
Copyright © Mimmo Cosenza, 2012-2014. Released under the Eclipse Public License, the same as Clojure.