/folio

Tutorial: Building web application on clojure (Work in progress!)

Creative Commons Zero v1.0 UniversalCC0-1.0

Folio

В этом кратком руководстве я хотел бы поделиться с сообществом своим опытом в постройке веб-приложения на языке Clojure.

Создание веб-приложения на Clojure

Ring

Ring - это маленькая clojure-библиотека, помогающая в написании веб-приложений. Ее создание навеяно такими знаменитыми библиотеками как WSGI для Python и Rack для Ruby. Концепция ring-приложения схожа с rack. У нас есть request - запрос, есть handler - обработчик запроса и response - HTTP-ответ. Request и response - это просто clojure map'ы. handler - это функция, аргументом которой является request, а возвращаемым значением response. handler единственный на все приложение, это другим словами точка входа. Для обработки response перед отправкой его в сеть клиенту (например, для указания дополнительных заголовков ответа) существует такое понятие как midleware - функция высшего порядка, аргументом которой является handler. Midleware в ring похожи на прослойки в rack. Приведу пример самого простого приложения на ring:

Сгенерируйте новый проект:

$ lein new hello-world

Укажите в зависимостях проекта ring-core и ring-jetty-adapter:

; project.clj

(defproject hello-world "0.1.0-SNAPSHOT"
  :description "FIXME: write description"
  :url "http://example.com/FIXME"
  :license {:name "Eclipse Public License"
            :url "http://www.eclipse.org/legal/epl-v10.html"}
  :dependencies [[org.clojure/clojure "1.8.0"]
                 [ring/ring-core "1.4.0"]
                 [ring/ring-jetty-adapter "1.4.0"]])

Тем самым мы говорим, что наш проект зависит от библиотеки ring-core и адаптера ring-jetty-adapter для java-веб-сервера Jetty. Адаптер служит посредником между вашим приложением и сервером при обмене запросами-ответами.

; src/hello-world/handler.clj 
(ns hello-world.handler)

(defn app
  [request]
  {:status 200
   :headers {"Content-Type" "text/html"}
   :body "Hello world!"})

Эта единственная функция и есть handler, ее достаточно для мини веб-приложения hello world. Возращаемое значение функции является map, обязательными ключами которого являются :status - код ответа: 200, 404, 500 и т.п. :headers - дополнительные заголовки ответа. Значением :headers также является map, ключ-значения которого обычные http-заголовки. :body - само тело ответа, которое может быть типа String, File или ISeq.

Испытаем наше первое приложение. Для этого зайдем в repl:

$ lein repl
(use 'ring.adapter.jetty)
(use 'hello-world.handler)

; запускаем jetty-сервер, указав первым аргументом наш handler, а вторым в виде map порт сервера
(run-jetty app {:port 3000})

Далее откроем браузер по адресу http://localhost:3000, и увидим наше приветствие.

lein-ring

Для того, чтобы наделить lein функционалом запуска вашего веб-приложения существует плагин lein-ring. Целью данного плагина является автоматизация рутинных задач для работы с ring, таких как запуск веб-сервера и указанию ring точки входа - то есть, handler'а. Дополним наше hello-world приложение плагином lein-ring:

; project.clj

(defproject hello-world "0.1.0-SNAPSHOT"
  :description "FIXME: write description"
  :url "http://example.com/FIXME"
  :license {:name "Eclipse Public License"
            :url "http://www.eclipse.org/legal/epl-v10.html"}
  :dependencies [[org.clojure/clojure "1.8.0"]
                 [ring/ring-core "1.4.0"]
                 [ring/ring-jetty-adapter "1.4.0"]]
  :plugins [[lein-ring "0.9.7"]]
  ; укажем handler:
  :ring {:handler hello-world.handler/app})

А теперь запустим наше приложение:

$ lein ring server

Добавляем логирование: ring-logger

Всякое приложение не обходится без логирования запросов. В помощь в разработке существует библиотека ring-logger. Я буду использовать ring-logger с бэкендом timbre ring-logger-timbre. Подключить данную библиотеку к нашему приложению не составит большого труда:

; project.clj

(defproject hello-world "0.1.0-SNAPSHOT"
  :description "FIXME: write description"
  :url "http://example.com/FIXME"
  :license {:name "Eclipse Public License"
            :url "http://www.eclipse.org/legal/epl-v10.html"}
  :dependencies [[org.clojure/clojure "1.8.0"]
                 [ring/ring-core "1.4.0"]
                 [ring/ring-jetty-adapter "1.4.0"]
				 [ring-logger-timbre "0.7.5"]]
  :plugins [[lein-ring "0.9.7"]]
  :ring {:handler hello-world.handler/app})
  

Чтобы задействовать в приложении обернем handler в midleware wrap-with-logger:

; src/hello-world/handler.clj

(ns hello-world.handler
  (:require [ring.logger.timbre :as logger.timbre]))

(defn handler
  [request]
  {:status 200
   :headers {"Content-Type" "text/html"}
   :body "<h1>Hello Julia!</h1>"})

(def app
  (logger.timbre/wrap-with-logger handler))
  

Запускаем и в консоли можно увидеть, как логируется каждый запрос:

screen

Отдаем статические файлы: css,js и т.п.

Многие веб-приложения имеют красиво оформленную страничку. Всю красоту обычно наводят с помощью css, html, JavaScript и картинок. Для начала нам необходимо научиться отдавать статичные файлы клиенту. Определимся, где будем хранить всю статику. Обычно для ring-приложения это директория resources/public. Пойдем по стандартному пути: создадим директории: $ mkdir -p resources/public/css resources/public/js resources/public/images. В качестве примера скачаем базовые стили для responsive веб-приложений skeleton.css. Помещаем skeleton.css и normalize.css в директорию resources/public/css. Мы будем ожидать, что наши css-файлы будут доступны по адресу localhost:3000/css/normalize.css. Аналогично для картинок, js и вообще для всего, что положим в resources/public. Добавим midleware для обслуживания статики в наше приложение:

; src/hello-world/handler.clj

(ns hello-world.handler
  (:require [ring.logger.timbre :as logger.timbre]
             ; "подключаем" middleware для статики
            [ring.middleware.resource :refer :all]))

(defn handler
  [request]
  {:status 200
   :headers {"Content-Type" "text/html"}
   :body "<h1>Hello Julia!</h1>"})

; "->" - это такой удобный макрос, "синтаксический сахар" clojure.
; Он "преобразует" такую конструкцию в более читабельный вид:
; (wrap-resource (logger.timbre/wrap-with-logger handler))
; при этом стоит учесть, что такая "одинарная" стрелка
; "прокидывает" handler в первый аргумент последующих форм.
; Существует еще макрос ->> , подробности ищите в документации clojure.
(def app
  (-> handler
      logger.timbre/wrap-with-logger
      (wrap-resource "public")))

Сейчас можно запустить сервер и открыть в браузере localhost:3000/css/normalize.css. Вы увидете исходный текст css стилей.

Hiccup: добавляем шаблонизатор

Для отображения веб-странички с динамическим контентом необходима функция, которая распарсит html-шаблон с некоторыми метками и заменит метки на нужный нам текст. В clojure наибольшей популярностью славится шаблонизатор hiccup. Данная библиотека служит для конвертации clojure-объектов в html-представление. Простейшим примером шаблона может служить следующий вектор:

(defn index-page
  []
  [:div {:class "container} "Hello world!"])

Что можно переконвертировать в html с помощью hiccup:

user=> (use 'hiccup.core)
nil
user=> (html (index-page))
"<div class=\"container\">Hello world!</div>"

Comming soon...

Compojure: маршрутиризация

Одностраничными сайтами бывают обычно сайты-визитки, рекламные и т.п. простые решения. Более сложные информационные ресурсы состоят из нескольких страниц. Задачу определения маршрутиризации uri-путей в clojure поможет маленькая библиотека compojure, которую почему-то зовут фреймворком. Далее я расскажу принцип ее работы, и как происходит парсинг путей. Сейчас чтобы не скучать, приведу сразу пример использования compojure в нашем Hello world проекте:

; project.clj

(defproject hello-world "0.1.0-SNAPSHOT"
  :description "FIXME: write description"
  :url "http://example.com/FIXME"
  :license {:name "Eclipse Public License"
            :url "http://www.eclipse.org/legal/epl-v10.html"}
  :dependencies [[org.clojure/clojure "1.8.0"]
                 [ring/ring-core "1.4.0"]
                 [ring/ring-jetty-adapter "1.4.0"]
                 [ring-logger-timbre "0.7.5"]
				 ; подключаем compojure
				 [compojure "1.5.1"]]
  :plugins [[lein-ring "0.9.7"]]
  :ring {:handler hello-world.handler/app})

Теперь необходимо перевести наш handler на работу с compojure. Для этого мы должны предоставить "карту маршрутов" на макросах из библиотеки compojure:

; src/hello-world/handler.clj

(ns hello-world.handler
  (:require [ring.logger.timbre :as logger.timbre]
            [compojure.core :refer :all]
			[compojure.route :as route]))

; старый handler запросов 
; (defn handler
;   [request]
;   {:status 200
;    :headers {"Content-Type" "text/html"}
;    :body "<h1>Hello World!</h1>"})

; новый handler сформируется из карты маршрутов. 
; Еще раз напомню, что ring мы должны передать функцию-обработчик, 
; которая при выполнении в контексте запроса сформирует ответ. 
; Макросы GET, POST и т.п. можно представить как функции-прослойки, 
; которые выполнят свой последний аргумент только в том случае, если uri совпадет с заданным.
(def application-routes
  (routes
    (GET "/" [] (str "<h1>Hello World!</h1>"))
	; в случае если пользователь перейдет по урлу не совпадающему ни с одним указанным маршрутом, ему вернется ответ 404
	(route/not-found (str "<h1>Page not found</h1>"))))
	
(def app
  (logger.timbre/wrap-with-logger application-routes))

Comming soon...