Сегодня я попробую показать основы создания веб приложений на языке Clojure. Здесь не будет сложной логики и модных фреймворков. Будет использоваться ряд библиотек для работы с примитивами. По мере упоминания я попробую в двух словах объяснить, какой функционал они предоставляют.
Архитектура веб-приложений в примитивах состоит из веб-сервера, который направляет запросы на обработчики в зависимости от пути, параметров, метода. Обработчик выполняет определенный код, делает запросы к базе данных, работает с файловой системой. После обработки запроса, генерируется ответ и отсылается клиенту.
Наше приложение будет принимать через форму одно значение, брать из базы данных второе, складывать их, а результат отдавать клиенту. При этом введенное значение будет заменять старое в базе данных. Глупая, бесполезная и не интересная логика — я знаю.
Для разработки приложения на Clojure нам понадобится, разумеется, Clojure и ряд вспомогательных библиотек. Первым делом качаемCloj, ВНЕЗАПНО, Leiningen. Действительно, чтобы писать на Clojure, можно не устанавливать сразу Clojure, а скачать Leiningen. Это утилита для сборки проектов на Clojure с поддержкой обычного ряда задач, включая зависимости, и расширяемая плгинами. За подробностями отправляю на страницу проекта или к Алексу Отту.
Итак, приступим:
Что же только что произошло? Leiningen создал папку проекта со следующей иерархией:
В первую очередь нас интересует файл project.clj. Структурно, он содержит обычный исходний код на Clojure:
Leiningen использует содержимое этого файла для работы с проектом. Есть возможность указания различных директив, о которых можно почитать в документации. Нас в первую очередь интересует раздел зависимостей. В нем можно указать библиотеки, которые используются в приложении. При выполнении комманды lein deps, Leiningen самостоятельно поместит все зависимости в папку lib/, а в случае необходимости, скачает их из репозитория.
Мы будем использовать ряд библиотек, которые нужно указать в разделе :dependencies:
Для отладки приложения нам понадобится плагин к Leiningen. Просто добавьте в project.clj еще одну секцию :dev-dependencies.
Теперь он выглядит вот так:
Далее откроем файл src/web_clojure_demo/core.clj. Пока он содержит только объявление пространста имен. Добавим в него необходимые зависимости и следующий код:
Рядом поместим шаблон index.html:
Как вы заметили, он не содержит никаких специальных тегов.
Далее через консоль нужно обновить зависимости и запустить отладочный веб-сервер:
Если вернуться к project.clj, можно заметить, что мы добавили аттрибут :ring {:handler web-clojure-demo.core/engine}. Он позволяет внутреннему веб-серверу Leiningen во время тестирования приложения направлять все запросы нашему обработчику. Это очень удобно, поскольку этот плагин Leiningen использует несколько заглушек, которые, например, позволяют обновлять исходный код без перезагрузки веб-сервера.
Давайте разберемся, что же происходит внутри.
Этот код получает содинение с хранилищем Redis. Переменную db мы будем дальше использовать для работы.
Этот код определяет обработчики различных запросов. В данном случае используются библиотеки compojure. Макрос site создает функцию-хендлер, которая поддерживает необходимый функционал для работы типичных сайтов — сессии, куки, параметры и др. В main-routes указан список радичных запросов и функции-обработчики. В случае, если ниодин из обработчиков не подходит, срабатывает not-found.
Это функции, которые обрабатывают наши запросы. Первая из них берет значение в базе и вызывает функцию генерации ответа из шаблона, вторая складывает имеющееся в базе значение и переданное и также вызывает генерацию ответа.
Макрос deftemplate создает функцию, которая принимает параметр ctxt, загружает html-шаблон и преобразует в соответствии с заданными правилами, в нашем случае это задание содержимого или изменение стилей. Библиотека Enlive позволяет производить куда более интересные манипуляции с html.
Вот как выглядит работа нашего приложения:






Oops!
Исправим это недоразумение. Отредактируем код:
Теперь наше приложение корректно обрабатывает неподходящие данные. В случае ошибки, клиенту будет показано сообщение.

Если я плохо осветил какую-либо часть, просьба сообщить мне об этом — статью дополню. В случае вопросов, просьба не стесняться. Здоровая и не очень критика также приветствуется.
Исходные коды этого примера можно найти в репозитории.
Архитектура веб-приложений в примитивах состоит из веб-сервера, который направляет запросы на обработчики в зависимости от пути, параметров, метода. Обработчик выполняет определенный код, делает запросы к базе данных, работает с файловой системой. После обработки запроса, генерируется ответ и отсылается клиенту.
Наше приложение будет принимать через форму одно значение, брать из базы данных второе, складывать их, а результат отдавать клиенту. При этом введенное значение будет заменять старое в базе данных. Глупая, бесполезная и не интересная логика — я знаю.
Для разработки приложения на Clojure нам понадобится, разумеется, Clojure и ряд вспомогательных библиотек. Первым делом качаем
Итак, приступим:
D:\dev\clojure>lein new web-clojure-demo Created new project in: D:\dev\clojure\web-clojure-demo
Что же только что произошло? Leiningen создал папку проекта со следующей иерархией:
src/ web_clojure_demo/ core.clj test/ web_clojure_demo/ test/ core.clj .gitignore README project.clj
В первую очередь нас интересует файл project.clj. Структурно, он содержит обычный исходний код на Clojure:
(defproject web-clojure-demo "1.0.0-SNAPSHOT" :description "FIXME: write description" :dependencies [[org.clojure/clojure "1.2.1"]])
Leiningen использует содержимое этого файла для работы с проектом. Есть возможность указания различных директив, о которых можно почитать в документации. Нас в первую очередь интересует раздел зависимостей. В нем можно указать библиотеки, которые используются в приложении. При выполнении комманды lein deps, Leiningen самостоятельно поместит все зависимости в папку lib/, а в случае необходимости, скачает их из репозитория.
Мы будем использовать ряд библиотек, которые нужно указать в разделе :dependencies:
- clojure-contrib — содержит различный полезный функционал, не вошедший в состав стандартной библиотеки языка: функции для работы со строками и потоками ввода/вывода, дополнительные функции для работы с коллекциями, монады и т.д.
- ring — библиотека, предоставляющая ряд абстракций над HTTP.
- compojure — содержит набор макросов и функций для создания веб-приложений. Является оберткой над ring.
- clj-redis — клиентская библиотека для Redis. В данном примере я буду использовать это NoSQL хранилище. Для реляционных решений советую посмотреть в сторону ClojureQL.
- enlive — библиотека для создания HTML ответа клиенту. Примечательна тем, что полностью выносит логику из шаблонов.
Для отладки приложения нам понадобится плагин к Leiningen. Просто добавьте в project.clj еще одну секцию :dev-dependencies.
Теперь он выглядит вот так:
(defproject web-clojure-demo "1.0.0-SNAPSHOT" :description "FIXME: write description" :dependencies [[org.clojure/clojure "1.2.1"] [org.clojure/clojure-contrib "1.2.0"] [ring/ring-jetty-adapter "0.2.5"] [compojure "0.6.2"] [clj-redis "0.0.9"] [enlive "1.0.0-SNAPSHOT"]] :dev-dependencies [[lein-ring "0.4.0"]] :ring {:handler web-clojure-demo.core/engine})
Далее откроем файл src/web_clojure_demo/core.clj. Пока он содержит только объявление пространста имен. Добавим в него необходимые зависимости и следующий код:
(ns web-clojure-demo.core (:use compojure.core) (:use [ring.adapter.jetty :only [run-jetty]]) (:use [ring.util.response]) (:require [compojure.route :as route] [compojure.handler :as handler] [clj-redis.client :as redis] [net.cgrand.enlive-html :as html])) (def db (redis/init {:url "redis://127.0.0.1:6379"})) (defn parse-input [a] (Integer/parseInt a)) (html/deftemplate page-index "web_clojure_demo/index.html" [ctxt] [:title] (html/content "Awesome application") [:#old] (html/content (:old ctxt)) [:#msg2] (html/set-attr "style" "display: none")) (html/deftemplate page-summary "web_clojure_demo/index.html" [ctxt] [:title] (html/content "Awesome application") [:#old] (html/content (:old ctxt)) [:#msg2] (html/content (str "Summary is " (:sum ctxt)))) (defn summary [value] (let [old (redis/get db "value")] (redis/set db "value" value) (page-summary { :sum (+ (parse-input value) (parse-input old)) :old old}))) (defn index [] (let [old (redis/get db "value")] (page-index {:old old}))) (defroutes main-routes (GET "/" [] (index)) (POST "/some_action" [value] (summary value)) (route/not-found "Page not found")) (def engine (handler/site main-routes))
Рядом поместим шаблон index.html:
<html> <head> <title></title> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> </head> <body> <div> <div id="msg1">Old value: <span id="old" /></div> <div id="msg2" /> <form method="post" action="/some_action" > <input type="text" name="value" ></input><input type="submit" name="ok" ></input> </form> </div>
Как вы заметили, он не содержит никаких специальных тегов.
Далее через консоль нужно обновить зависимости и запустить отладочный веб-сервер:
D:\dev\clojure\web-clojure-demo>lein deps Copying 19 files to D:\dev\clojure\web-clojure-demo\lib Copying 17 files to D:\dev\clojure\web-clojure-demo\lib\dev D:\dev\clojure\web-clojure-demo>lein ring server 2011-03-31 22:23:25.125::INFO: Logging to STDERR via org.mortbay.log.StdErrLog 2011-03-31 22:23:25.125::INFO: jetty-6.1.14 2011-03-31 22:23:25.203::INFO: Started SocketConnector@0.0.0.0:3000 Started server on port 3000
Если вернуться к project.clj, можно заметить, что мы добавили аттрибут :ring {:handler web-clojure-demo.core/engine}. Он позволяет внутреннему веб-серверу Leiningen во время тестирования приложения направлять все запросы нашему обработчику. Это очень удобно, поскольку этот плагин Leiningen использует несколько заглушек, которые, например, позволяют обновлять исходный код без перезагрузки веб-сервера.
Давайте разберемся, что же происходит внутри.
(def db (redis/init {:url "redis://127.0.0.1:6379"}))
Этот код получает содинение с хранилищем Redis. Переменную db мы будем дальше использовать для работы.
(defroutes main-routes (GET "/" [] (index)) (POST "/some_action" [value] (summary value)) (route/not-found "Page not found")) (def engine (handler/site main-routes))
Этот код определяет обработчики различных запросов. В данном случае используются библиотеки compojure. Макрос site создает функцию-хендлер, которая поддерживает необходимый функционал для работы типичных сайтов — сессии, куки, параметры и др. В main-routes указан список радичных запросов и функции-обработчики. В случае, если ниодин из обработчиков не подходит, срабатывает not-found.
(defn index [] (let [old (redis/get db "value")] (page-index {:old old}))) (defn summary [value] (let [old (redis/get db "value")] (redis/set db "value" value) (page-summary { :sum (+ (parse-input value) (parse-input old)) :old old})))
Это функции, которые обрабатывают наши запросы. Первая из них берет значение в базе и вызывает функцию генерации ответа из шаблона, вторая складывает имеющееся в базе значение и переданное и также вызывает генерацию ответа.
(html/deftemplate page-index "web_clojure_demo/index.html" [ctxt] [:title] (html/content "Awesome application") [:#old] (html/content (:old ctxt)) [:#msg2] (html/set-attr "style" "display: none")) (html/deftemplate page-summary "web_clojure_demo/index.html" [ctxt] [:title] (html/content "Awesome application") [:#old] (html/content (:old ctxt)) [:#msg2] (html/content (str "Summary is " (:sum ctxt))))
Макрос deftemplate создает функцию, которая принимает параметр ctxt, загружает html-шаблон и преобразует в соответствии с заданными правилами, в нашем случае это задание содержимого или изменение стилей. Библиотека Enlive позволяет производить куда более интересные манипуляции с html.
Вот как выглядит работа нашего приложения:






Oops!
Исправим это недоразумение. Отредактируем код:
(html/deftemplate page-summary "web_clojure_demo/index.html" [ctxt] [:title] (html/content "Awesome application") [:#old] (html/content (:old ctxt)) [:#msg2] (html/content (if (:error ctxt) (:error ctxt) (str "Summary is " (:sum ctxt))))) (defn summary [value] (let [old (redis/get db "value")] (try (let [ a (parse-input value) b (parse-input old)] (redis/set db "value" value) (page-summary { :sum (+ a b) :old old})) (catch NumberFormatException e (page-summary {:old old :error "Number Format Exception"})))))
Теперь наше приложение корректно обрабатывает неподходящие данные. В случае ошибки, клиенту будет показано сообщение.

Заключение
Если я плохо осветил какую-либо часть, просьба сообщить мне об этом — статью дополню. В случае вопросов, просьба не стесняться. Здоровая и не очень критика также приветствуется.
Исходные коды этого примера можно найти в репозитории.
