Chapter 10. Integration with Clojure

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.

AJAX

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.

The Reader and Printer

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.

Table 10-1. JSON and Clojure data differences
FeatureJSONClojure
NumbersYesYes
StringsYesYes
Symbols-Yes
Keywords-Yes
Lists (Arrays)YesYes
Maps with string keysYesYes
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.

Example Client-Server Application

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.

Screen shot of the demo application for this chapter
Figure 10-1. Screen shot of the demo application for this chapter

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.

Note

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.

Extending the Reader

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.

User-Defined Tagged Literals

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).

Sharing Code

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

Summary

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.



[9] This was discussed in a thread on the Google Closure Library mailing list.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.137.176.166