ClojureScript, as we have seen, is targeted primarily at web browsers. Although this makes it possible to design complete applications that run in a browser, it is even more powerful when combined with a web server running Clojure on the JVM. Clojure’s literal data structures provide a rich data format for communication between a client and server, and with a little care you can even share code between the two languages.
In spite of its original definition, Asynchronous JavaScript and
XML, AJAX has become a catch-all term for rich client
applications running in web browsers, communicating with a web server. The
Google Closure Library provides the goog.net.XhrIo
class to
support asynchronous HTTP requests to a server across many different
browser implementations.
Here is a simple example function that performs an HTTP POST request to a server:
(ns example (:require [goog.net.XhrIo :as xhr])) (defn receiver [event] (let [response (.-target event)] (.write js/document (.getResponseText response)))) (defn post [url content] (xhr/send url receiver "POST" content))
The goog.net.XhrIo/send
function takes a URL, a
callback function, a method name, and an optional request body. When the
server responds to the request, it will invoke the callback function on an
object from which you can retrieve the status code, headers, and response
body sent by the server.
The goog.net.XhrIo
class and the associated
goog.net.XhrManager
class provide many more options for
controlling HTTP server requests. Covering all of them is outside the
scope of this book, but for more information you can consult the Google
Closure Library[5] or Chapter 7 of Michael Bolin’s Closure: The
Definitive Guide (O’Reilly). In addition, some ClojureScript
libraries are growing to support easier access to the HTTP features in the
Google Closure Library; see Appendix A for details.
Although they started with XML, many web browser applications now
use JSON (JavaScript Object Notation) for
communication between client and server. You can use JSON in ClojureScript
as well: the Google Closure Library class
goog.json.Serializer
can serialize data to and from JSON, and
there are several JSON libraries for Clojure.
However, JSON is a feeble data format when compared with Clojure’s own literal syntax. It cannot distinguish between strings and keywords, and its maps (objects) only support strings as keys. Almost any application using JSON as a data format will eventually need to translate between native application data structures and their “lossy” JSON representations.
Clojure’s data structures, on the other hand, are rich enough to represent almost any application domain, and they have a string representation that is just as compact as JSON. Furthermore, Clojure’s literal data syntax is extensible, which we will explore later in this chapter.
Table 10-1 highlights the differences between JSON and Clojure data.
Feature | JSON | Clojure |
Numbers | Yes | Yes |
Strings | Yes | Yes |
Symbols | - | Yes |
Keywords | - | Yes |
Lists (Arrays) | Yes | Yes |
Maps with string keys | Yes | Yes |
Maps with arbitrary keys | - | Yes |
Sets | - | Yes |
Metadata | - | Yes |
Extensibility | - | Yes |
Like any LISP-like language, both Clojure and ClojureScript have a
reader, a function that transforms a stream of
characters into data structures such as lists, maps, and sets. The
ClojureScript compiler uses the same reader as the Clojure language
runtime. The Clojure reader (invoked through the functions
read
and read-string
) is implemented in the Java
language, so it is not available to ClojureScript programs. But
ClojureScript has its own reader, implemented in ClojureScript, which is designed to be fully
compatible with the Clojure reader.
The ClojureScript reader is invoked through the function
cljs.reader/read-string
. As the name suggests, it takes a
string argument and returns a single data structure read from that
string:
(ns example (:require [cljs.reader :as reader])) (reader/read-string "{:a 1 :b 2}") ;;=> {:a 1, :b 2}
The opposite of read-string
is the built-in
ClojureScript function pr-str
, or “print to string,” which
takes a data structure and returns its string representation:
(pr-str {:language "ClojureScript"}) ;;=> "{:language "ClojureScript"}"
Notice that pr-str
automatically escapes special
characters and places strings in double quotes, which the
print
and println
functions do not:
(println {:language "ClojureScript"}) ;; {:language ClojureScript} ;;=> nil
In general, the print
, println
, and
str
functions are used for human-readable output, whereas the
pr
, prn
, and pr-str
functions are
used for machine-readable output.
Building a complete client-server application in Clojure and ClojureScript requires some knowledge of Clojure web libraries, which are outside the scope of this book. But the following example should give you an idea of how easy it is to communicate between the two languages.
This simple application will allow you to type Clojure expressions into a web form, evaluate them on the server, and display the result back in the web page.
Create a new project directory with the following
project.clj
file:
(defproject client-server "0.1.0-SNAPSHOT" :plugins [[lein-cljsbuild "0.2.7"]] :dependencies [[org.clojure/clojure "1.4.0"] [org.clojure/clojurescript "0.0-1450"] [domina "1.0.0"] [compojure "1.1.0"] [ring/ring-jetty-adapter "1.1.1"]] :source-paths ["src/clj"] :main client-server.server :cljsbuild { :builds [{ :source-path "src/cljs" :compiler { :output-to "resources/public/client.js" :optimizations :whitespace :pretty-print true}}]})
Our application will use the Clojure libraries Ring[6] and Compojure[7] for the server side of the application, and the
ClojureScript library Domina[8] for the client. Here is the server implementation, in the
file src/clj/client_server/server.clj
:
(ns client-server.server (:require [compojure.route :as route] [compojure.core :as compojure] [ring.util.response :as response] [ring.adapter.jetty :as jetty])) (defn eval-clojure [request] (try (let [expr (read-string (slurp (:body request)))] (pr-str (eval expr))) (catch Throwable t (str "ERROR: " t)))) (compojure/defroutes app (compojure/POST "/eval" request (eval-clojure request)) (compojure/GET "/" request (response/resource-response "public/index.html")) (route/resources "/")) (defn -main [] (prn "View the example at http://localhost:4000/") (jetty/run-jetty app {:join? true :port 4000}))
Next, the client side, at
src/cljs/client_server/client.cljs
:
(ns client-server.client (:require [goog.net.XhrIo :as xhr] [domina :as d] [domina.events :as events])) (def result-id "eval-result") (def expr-id "eval-expr") (def button-id "eval-button") (def url "/eval") (defn receive-result [event] (d/set-text! (d/by-id result-id) (.getResponseText (.-target event)))) (defn post-for-eval [expr-str] (xhr/send url receive-result "POST" expr-str)) (defn get-expr [] (.-value (d/by-id expr-id))) (defn ^:export main [] (events/listen! (d/by-id button-id) :click (fn [event] (post-for-eval (get-expr)) (events/stop-propagation event) (events/prevent-default event))))
Finally, we need an HTML page to contain the application:
<html> <head> <title>ClojureScript Client-Server Example</title> </head> <body> <h1>ClojureScript Client-Server Example</h1> <form id="eval-form"> <p><label for="eval-expr"> Enter a Clojure expression to evaluate on the server: </label></p> <p><input id="eval-expr" name="eval-expr" type="text" size="70" /></p> <p><input id="eval-button" type="button" value="Eval" /></p> </form> <p>The result:</p> <pre id="eval-result"> </pre> <script src="/client.js" language="javascript"></script> <script type="text/javascript" language="javascript"> client_server.client.main() </script> </body> </html>
This example is slightly different from most of the HTML in this
book: the <script>
tags are at the
bottom of the file rather than in the
<head>
. This is necessary because the main
function we defined in ClojureScript depends on the DOM elements for the
form already being available. If the script
tags were at the
top of the file, there would be no reported errors but the event handler
would never get attached to the Eval button and the application wouldn’t
work.
The Google Closure Library does not have an “on DOM ready” event as
is commonly found in other JavaScript libraries. This was a deliberate
choice for performance reasons: web browsers load JavaScript
synchronously, blocking other rendering tasks. If you have a large
<script>
at the top of your HTML file, the browser will
not render anything until that JavaScript has been downloaded, parsed, and
evaluated. The Google Closure development team actually advocates placing
<script>
tags inline with HTML, just after the elements
they depend on.[9] This approach yields maximum responsiveness but is
complicated to implement. Placing <script>
tags at the
end of the document is an easier alternative that works consistently and
is fast enough for most applications.
Once you have created the files for this application, you can
compile it with lein cljsbuild once
and run it with
lein run
. Visit http://localhost:4000/
in your
web browser and you should see an application page like Figure 10-1.
You can type an expression into the text box and click the Eval button to evaluate it. The result will appear below the form. Remember these expressions are being evaluated on the server, so they are in Clojure, not ClojureScript. You can see that by evaluating expressions that are only valid on the JVM, such as a BigInteger calculation:
(.pow (BigInteger. "2") 128)
Obviously this is a naïve implementation, and completely insecure. But it presents an idea of the possibilities of communicating between a client and server written in the same language, using the native data structures of that language as the data format.
You can even send ClojureScript expressions to the server, compile them with the ClojureScript compiler, and return JavaScript source code back to the browser for evaluation. ClojureScript’s browser-attached REPL uses this technique, as do some experimental hybrid development environments.
Session is an experimental browser-based REPL by Kovas Boguta; source code and a demo video are available. Himera, by Michael Fogus, presents the ClojureScript compiler as a web service; source code and a demo application are available.
Clojure 1.4.0 added extensibility to the reader in the form of tagged literals. A tagged literal is written as a hash (#) sign, followed by a symbol, followed by any other Clojure data structure. When the reader encounters a tagged literal, it looks up the tag in a table to find its associated reader function, then invokes that function to the following data structure as an argument. User code can define new tags and override the behavior of existing tags.
Clojure has a few built-in reader literals already, with more likely
to come. For example, the #inst
tag specifies an instant in
time as a string in RFC 3339
format, like this:
#inst "2012-07-19T18:46:35.886-00:00"
The key feature of tagged literals is that they specify a precise
literal representation but allow for different
in-memory representations. The string after #inst
must
conform to RFC 3339, but Clojure on the JVM can parse it into one of
several classes, such as java.util.Date
or
java.util.Calendar
. The ClojureScript reader will parse the
same instant literal into a JavaScript Date
. When
constructing a client-server application using both Clojure and
ClojureScript, you no longer need to worry about converting dates to and
from strings: you can print and read dates like any other native Clojure
data structure.
You can define your own reader literals as well. User-defined tags must be namespace-qualified symbols; all non-qualified symbols are reserved for future Clojure language extensions.
In Clojure on the JVM, the special file
data_readers.clj
contains a map from tag symbols to the
fully-qualified names of functions that read them. You can also locally
override the tagged literal functions by rebinding
*data-readers*
. In ClojureScript, you can add tagged
literal functions with the cljs.reader/register-tag-parser!
function, which takes a tag symbol and a function.
Keep in mind that tagged literal readers do not have access to the raw character stream. The Clojure(Script) reader will read in the characters that follow the tag, interpret them as a normal Clojure data structure, then invoke the tagged literal function on the data structure. The function should return a value, which replaces the tagged data structure in the final result.
Tagged literals are still a new feature in the Clojure language
ecosystem, and support is evolving. Right now there is no well-defined
API for printing tagged literals (in Clojure on the JVM, you can extend
print-method
to new types).
As we have mentioned several times throughout this book, one of ClojureScript’s strengths is that it is the same language as Clojure. As a result, you can share code between Clojure and ClojureScript. This is particularly powerful for client-server applications on the web. The same code can run on the client, compiled into JavaScript, as on the server, compiled into JVM bytecode.
As we have also stated repeatedly, shared code has to conform to a common subset of the features available in both environments. Code that does any of the following will not be shareable:
Calls methods or classes of the host environment
Interacts with host-environment resources such as the DOM
Uses features that are only implemented in one host environment (such as Clojure’s refs and vars, which ClojureScript does not support)
Depends on behavior peculiar to the host environment (such as JavaScript’s automatic conversion between strings and numbers)
Again, the point of ClojureScript is not to simulate Clojure and the JVM in a web browser. Clojure and ClojureScript are the same language, ported to different platforms. Clojure has been ported to other platforms, such as the .NET Common Language Runtime. Intrepid developers have even started modifying the ClojureScript compiler to emit code for other target languages including Scheme, Lua, and Objective C.
Techniques for sharing code between Clojure and ClojureScript are still evolving. In the simplest case, one can simply copy or symlink code in two directories. The lein-cljsbuild plug-in has a feature called crossovers to facilitate cross-language copying, as described in Chapter 9. If you want more precise control over how your code is compiled, you can invoke the ClojureScript compiler directly from Clojure. Future versions of Clojure and ClojureScript will likely include some kind of conditional evaluation or “feature expressions,” making it possible to maintain a single source file that targets multiple host environments.
In any case, the possibilities of having a unified language across servers and web browsers are exciting. Consider some examples:
The classic Model-View-Controller pattern, in which the Model can be mirrored on both client and server
Unit-testing client and server code in the same process
Debugging client code before running it in a browser
Being able to work in the same language and data model in both web browsers and web servers is the most compelling feature of ClojureScript. With a little care, most algorithmic or data-centric code can be made to work identically in Clojure and ClojureScript. As both languages continue to develop, they will converge towards a common core, making it even easier to write code that targets both environments.