Сегодня я попробую показать основы создания веб приложений на языке 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"})))))
Теперь наше приложение корректно обрабатывает неподходящие данные. В случае ошибки, клиенту будет показано сообщение.
Заключение
Если я плохо осветил какую-либо часть, просьба сообщить мне об этом — статью дополню. В случае вопросов, просьба не стесняться. Здоровая и не очень критика также приветствуется.
Исходные коды этого примера можно найти в репозитории.