Clojure 1.7 introduced reader conditionals and standardized way to write Clojure code for various platforms. Last few months ago, when I started to write Rigui, I decided to support both Clojure and ClojureScript. And it turns out to be possible. However, during the development, you need to be aware of a few differences between these two hosts, from tooling, syntax to host natures. That's just way more than reader conditionals.
Host differences
The most important difference between JVM and JavaScript runtime is multithread
support. At the moment, almost all JavaScript runtime are event-driven. For most
time there is only one thread you can run your code on. And when it comes to
multithread, like WebWorker, the platform offers a message-passing API and you
don't have any shared memory or data. So features like STM, Agent are not
necessary and completely unavailable in this runtime. In case you were using
dosync
or send!
in your Clojure code, you need to add :cljs
branch and
switch to atom
.
#?(:clj (dosync
(let [b (get (ensure (.-buckets parent)) trigger-time)]
(alter (.-buckets parent) dissoc trigger-time)
(ensure b)))
:cljs (let [b (get @(.-buckets parent) trigger-time)]
(swap! (.-buckets parent) dissoc trigger-time)
@b))
Due to the same reason, future
and promise
(the Clojure promise) are not
available in ClojureScript. If you were using a promise as derefable result in
API, you need to find other solution in ClojureScript.
Reader conditionals
The Clojure wiki has a brief intro for reader conditionals.
Just to add that if you have a namespace that has completely different
implementations, you can write them in different files. For example,
platform.clj
and platform.cljs
. When importing the namespace (require
'[platform]')
, the reader will look for right file for you.
Syntax differences
There are some minor syntax differences between clj and cljs. To access a field of an object or record, you will need to use (.-fieldName obj). Otherwise cljs will try to call it as a function. Actually this syntax is also supported in clj currently, so I recommend you to switch to this completely in all your clojure code.
Cljs requires explicit macro import. You will need (:require-macros)
in your
(ns)
header, or using :refer-macros
in a require form.
Tooling
If you were to write a library for clojurescript, you can use leiningen without plugins. Because cljs libraries are also distributed as jars so you don't need to generate JavaScript files. And in case you need to, cljsbuild is the de-facto plugin for cljs development.
Second, there is no perfect REPL workflow in ClojureScript as in Clojure, to be honest. It's a pain to test your cljs code in Clojure way and browser/node environment. If you have recommendation please tell me.
I thought tests were as pain as REPL until I found doo. doo is a lein plugin made cljs testing as easy as clojure. What you have to do is provide a runner namespace and generate js for this runner. No copy/paste required, no duplicated code. Without doo, you will need to generate JavaScript sources from you clojure test file using cljsbuild, ns by ns, and execute these files one by one. You can find my test runner here and its cljsbuild configuration.
Conclusion
These are my cents about developing library for both clj and cljs. The conclusion is simply it works. In 2016 you can, and you should be encouraged to write Clojure library with cross-platform in mind.