-
Notifications
You must be signed in to change notification settings - Fork 1
Clojure in the server
Now that we have things working within the browser, let's hit the highway and set up the project to work from a server, shall we? First, for some reorg:
- First, let's make space for the Clojure code that we will soon be creating. Create new folders
src/clj
andsrc/cljs
and move the existingclj_stack
undersrc/cljs
- Create a new folder for holding static files and resources -
resources/public
- Modify
project.clj
to update obsolete paths and include dependencies. Pedestal is a web application framework that helps in routing and serving HTTP requests while the underlying Ring framework takes care of low-level HTTP.
(defproject clj-stack "0.1.0-SNAPSHOT"
:description "FIXME: write this!"
:url "http://example.com/FIXME"
:dependencies [[org.clojure/clojure "1.6.0"]
[org.clojure/clojurescript "0.0-2755"]
[org.clojure/core.async "0.1.346.0-17112a-alpha"]
[org.omcljs/om "0.8.8"]
[io.pedestal/pedestal.service "0.3.1"]
[io.pedestal/pedestal.jetty "0.3.1"]
[ch.qos.logback/logback-classic "1.1.2" :exclusions [org.slf4j/slf4j-api]]
[org.slf4j/jul-to-slf4j "1.7.7"]
[org.slf4j/jcl-over-slf4j "1.7.7"]
[org.slf4j/log4j-over-slf4j "1.7.7"]
[org.clojure/tools.namespace "0.2.9"]]
:plugins [[lein-cljsbuild "1.0.4"]]
:source-paths ["src/clj" "src/cljs" "target/classes"]
:clean-targets ["resources/public/clj_stack" "resources/public/clj_stack.js"]
:main ^{:skip-aot true} clj_stack.server
:cljsbuild {
:builds [{:id "clj-stack"
:source-paths ["src/cljs"]
:compiler {
:output-to "resources/public/clj_stack.js"
:output-dir "resources/public"
:optimizations :none
:cache-analysis true
:source-map true}}]})
The server
is the running instance of our application. src\clj\clj_stack\server.clj
:
(ns clj_stack.server
(:gen-class) ; for -main method in uberjar
(:require [io.pedestal.http :as server]
[clj_stack.service :as service]
[clojure.tools.namespace.repl :refer [refresh]]))
;; This is an adapted service map, that can be started and stopped
;; From the REPL you can call server/start and server/stop on this service
(defonce runnable-service (server/create-server service/service))
The service
handles all incoming requests. src\clj\clj_stack\service.clj
:
(ns clj_stack.service
(:require [io.pedestal.http :as bootstrap]
[io.pedestal.http.route :as route]
[io.pedestal.http.body-params :as body-params]
[io.pedestal.http.route.definition :refer [defroutes]]
[ring.util.response :as ring-resp]))
(defn home-page
[request]
(ring-resp/response "Hello World"))
(defroutes routes
[[["/" {:get home-page}]]])
(def service {:env :prod
::bootstrap/routes routes
::bootstrap/resource-path "/public"
::bootstrap/type :jetty
::bootstrap/port 8080})
From the project dir, execute lein repl
. This will start the repl in the clj_stack.server
namespace (Note the :main clj_stack.server
configuration in project.clj
). Now, you can run (server/start-server runnable-service)
to start up the service. Point the browser to localhost:8080
and you will see a greeting again, only this time it comes over HTTP.
Before we can render our index.html
from the server, we need to alter some of the paths of the javascript dependencies so that they are fetched from the target of the cljs compilation (resources/public
):
<script src="goog/base.js" type="text/javascript"></script>
<script src="clj_stack.js" type="text/javascript"></script>
Alter the clj_stack.service/home-page
function to:
(defn home-page
[request]
(-> (ring-resp/file-response "index.html")
(ring-resp/content-type "text/html")))
This uses the ring
API to render the HTML file as the response and set the Content-Type of the response to text/html
.
But, for this to work, we need to reload the server. This is a 3-step process or stopping the server, reloading the namespace and starting the server again. Hit Ctrl+C
in the server repl to get the prompt and enter the following:
(server/stop runnable-service)
(refresh)
(server/start runnable-service)
Once the servers starts up again, open the browser at localhost:8080
and you will see our Fake YouTube app work in all its glory
Since we have a fully working server, it seems to be a shame to maintain the video list locally in the client. Especially since a browser refresh will undo all the hard work of adding new videos! In order to move the data model to the server-side, we need to add the following: an atom to track the model and a web-service function to return the videos in edn
format (EDN is considered an improvement from JSON and is handy here because it's a subset of the Clojure notation and therefore does not require any marshalling/unmarshalling logic)
(def data-model
(atom
{:videos
[{:id 1 :title "Intro to Datomic" :url "http://www.youtube.com/embed/RKcqYZZ9RDY"}
{:id 2 :title "The Functional Final Frontier" :url "http://www.youtube.com/embed/DMtwq3QtddY"}]}))
(defn videos [request]
(bootstrap/edn-response @data-model))
We also need to add a route that will respond to /videos
:
(defroutes routes
[[["/" {:get home-page}]
["/videos" {:get videos}]]])
Once you restart the server, try running this: curl http://localhost:8080/videos
and you will see the expected edn response from the server.
In order to update the client to use this data API, we will be using a library called om-sync
which will come in handy later when we need to automate the communication between the client and the server. For now, add these dependencies:
project.clj - [om-sync "0.1.1"]
(under :dependencies
)
core.cljs - [om-sync.util :refer [edn-xhr]]
(as a :require
clause)
To finish, replace the (om/root ..)
call at the end to the following instead:
(edn-xhr
(let [target (. js/document (getElementById "app"))]
{:method :get
:url "/videos"
:on-complete #(om/root app-view % {:target target})}))
edn-xhr
makes an asynchronous request to the said URL and on receiving a response calls the callback function which roots the view to the application model received
By emptying the client-side application model and refreshing the browser, you can verify that the data is indeed initialized from the server:
; core.cljs
(def app-model (atom {:videos []}))
Since the data-model is now maintained in the server, we need to update the server for every change that happens in the client-side application model.
Let's start by adding a couple of URL endpoints and corresponding handlers for adding and deleting videos
; service.clj
(defn delete-video [request]
(let [id (Integer/parseInt (get-in request [:path-params :id]))]
(swap! data-model
(fn [d]
(update-in d [:videos]
#(for [el % :when (not= id (:id el))] el))))
(bootstrap/edn-response {:id id :data data-model})))
(defn add-video [request]
(let [video (:edn-params (body-params/edn-parser request))]
(swap! data-model (fn [d] (update-in d [:videos] #(conj % video))))
(ring-resp/response "ok")))
(defroutes routes
[[["/" {:get home-page}]
["/videos" {:get videos :post add-video}]
["/videos/:id" {:delete delete-video}]]])
The client requires corresponding changes while responding to the add and remove actions, whereby a edn-xhr
asynchronous call relays the change in the application model to the server
; core.cljs
(defn add-video [videos owner]
(let [title (-> (om/get-node owner "new-video-title") .-value)
url (-> (om/get-node owner "new-video-url") .-value)
video-data {:id (rand-int 1000) :title title :url url}]
(om/transact! videos #(conj % video-data))
(println "app-model: " @app-model)
(edn-xhr {:url "/videos" :method :post :data video-data})
(om/set-state! owner :new-video-name "")
(om/set-state! owner :new-video-url "")))
(defn delete-video [videos del-video]
(om/transact! videos
(fn [vs] (vec (remove #(= % del-video) vs))))
(edn-xhr {:url (str "/videos/" (:id del-video)) :method :delete}))
Make sure you play with the application to convince yourself that the data model changes in accordance with the user action. Any server restart should reset the model state to its initial value. Now, all that remains is for us to persist this data model and attempt some analysis on it. And that brings us to Clojure in the Database