It’s a common pattern to write client-server applications with a JavaScript client. The inventor of Clojure, Rich Hickey, said at the announcement of ClojureScript, “Clojure rocks, JavaScript reaches.”1 He meant that it is great to be able to write Clojure that runs on the server JVM and the client browser in JavaScript. We’re going to work through a pattern for doing this.
1. ClojureScript Announcement, July 2011, https://www.youtube.com/watch?v=tVooR-dF_Ag
In this recipe we will create a client in ClojureScript that will make a REST call. Then we will reuse the REST server from the previous chapter to receive the call. We will see the entire process end to end.
In this chapter we assume the following:
You know about ClojureScript and how Clojure can be compiled into JavaScript.
You understand that you can make calls to RESTful services from JavaScript.
You understand basic command prompt operations, such as renaming files.
You understand that that we can include JavaScript inside HTML files in order to run them.
You have installed Chrome as a web browser.
You’ll get to consume a REST web service client-side in a browser, using ClojureScript. You’ll see ClojureScript compiled to JavaScript, and see it running. You’ll see how ClojureScript can interact with other JavaScript libraries like the Google Closure JavaScript libraries.
1. Create a minimal ClojureScript application with
lein new mies cljs-rest-client-server-demo
2. Ensure that the project.clj
file contains the following:
(defproject cljs-rest-client-server-demo "0.1.0-SNAPSHOT"
:dependencies [[org.clojure/clojure "1.7.0-beta2"]
[org.clojure/clojurescript "0.0-3308"]
[ring/ring-core "1.4.0-RC1"]
[ring/ring-json "0.3.1"]]
:node-dependencies [[source-map-support "0.2.8"]]
:plugins [[lein-cljsbuild "1.0.6"]
[lein-ring "0.9.5"]]
:ring {:handler cljs-rest-client-server-demo.handler/app}
:source-paths ["src" "target/classes"]
:clean-targets ["out/cljs_rest_client_server_demo"
"cljs_rest_client_server_demo.js"
"cljs_rest_client_server_demo.min.js"]
:cljsbuild {
:builds [{:id "dev"
:source-paths ["src-cljs"]
:compiler {
:main cljs-rest-client-server-demo.core
:output-to "out/cljs_rest_client_server_demo.js"
:output-dir "out"
:optimizations :none
:cache-analysis true
:source-map true}}
{:id "release"
:source-paths ["src-cljs"]
:compiler {
:main cljs-rest-client-server-demo.core
:output-to "out-adv/cljs_rest_client_server_demo.min.js"
:output-dir "out-adv"
:optimizations :advanced
:pretty-print false}}]})
3. Rename the directory src to src-cljs
.
4. Modify the file src-cljs/cljs_rest_client_server_demo/core.cljs
and ensure it contains the following:
(ns cljs-rest-client-server-demo.core
(:require [clojure.browser.repl :as repl]
[goog.net.XhrIo :as xhr]
[goog.Uri.QueryData :as query-data]
[goog.structs :as structs]
[goog.dom :as dom]))
(enable-console-print!)
(defn receiver [event]
(let [response (.-target event)]
(println (.getResponseText response))
(set! (.-value (dom/getElement "returnVal"))
(.getResponseText response))))
(defn post [url content]
(xhr/send url receiver "POST" content))
(defn ^:export main [& _]
(println "starting!")
(post "/4459"
(query-data/createFromMap
(structs/Map. (clj->js {:mykey
(.-value (dom/getElement "returnVal"))}))))
(println "done"))
5. In the project directory, run the following command:
lein cljsbuild once
6. Next create the subdirectories resources/public/
under the project directory.
7. Copy the file index.html
to the resources/public
directory.
8. Then copy the directory out
to the resources/public
directory.
9. Create the subdirectory src/cljs_rest_client_server_demo
under the project directory.
10. Create the file src/cljs_rest_client_server_demo/handler.clj
and ensure it contains the following:
(ns cljs-rest-client-server-demo.handler
(:require [ring.util.response :as ring-res]
[compojure.route :as route]
[compojure.handler :as handler]
[ring.middleware.resource :as resources]
[ring.middleware.params :as params])
(:use compojure.core
ring.middleware.file-info))
(defroutes app-routes
(PUT "/:id" {params :params}
(str "put called with params: " params))
(POST "/:id" {params :params}
(str "post called with params: " params))
(route/resources "/")
(route/not-found
"<a href='/index.html'>Try it</a>"))
(def app
(-> app-routes
(params/wrap-params)
handler/api
(resources/wrap-resource "public")
(wrap-file-info)))
11. Modify the file resources/public/index.html
to contain the following:
<html>
<body>
<script src="out/cljs_rest_client_server_demo.js"
type="text/javascript"></script>
<button onclick="cljs_rest_client_server_demo.core.main()">Try it
</button>
Return Value: <input type="text" id="returnVal"><br>
</body>
</html>
To test the solution, follow these steps:
1. In the command prompt in the project directory, start the REST and web server with ring on port 4000:
lein ring server 4000
This should open a new web browser window with the following URL:
http://localhost:4000/index.html
2. Right-click on the page in Chrome and select Inspect element.
3. Select the tab Console.
4. Click the Try it button on the web page.
5. Observe the following entry in the console:
starting!
done
post called with params: {:id "4459", :mykey "42"}
In the project.clj
file, note in particular the :source-paths
parameter. It’s looking for a directory called src
for Clojure and src-cljs
for ClojureScript. This is the prompt to the Clojure and ClojureScript compilers to tell them where to find the ClojureScript files.
:source-paths ["src" "target/classes"]
...
:builds [{:id "dev"
:source-paths ["src-cljs"]
...
{:id "release"
:source-paths ["src-cljs"]
Note in the file src/core.cljs
that we use the line (xhr/send url receiver "POST" content)
to send a post request to the server using Google’s Closure library.
(defn post [url content]
(xhr/send url receiver "POST" content))
Then we send debugging information to the JavaScript in the console using the following line (println "starting!")
. You can view this in your Chrome browser by right-clicking on a page, selecting Inspect Element, and then clicking the Console tab. Note that we enabled (println
in ClojureScript using the expression (enable-console-print!)
. Without this, we would have had to use (.log js/console "starting!")
.
(defn ^:export main [& _]
(println "starting!")
(post "/4459"
(query-data/createFromMap
(structs/Map. (clj->js {:mykey
(.-value (dom/getElement "returnVal"))}))))
(println "done"))
In the file src/cljs_rest_client_server_demo/handler.clj
we write our server. Note in particular the line above wrap-file-info
. This is middleware to ensure that mime types are set correctly when files are requested.
(def app
(-> app-routes
(params/wrap-params)
handler/api
(resources/wrap-resource "public")
(wrap-file-info)))
In this chapter we have done the following:
Built a simple REST server to respond to our requests.
Built a ClojureScript client to call our REST server.
Kicked off the ClojureScript client from an HTML page loaded into a web browser.
Observed the result of the client calling the server in the console of the browser.
18.220.202.209