Pull to refresh

Основы Clojure Web Applications

Reading time 6 min
Views 11K
Сегодня я попробую показать основы создания веб приложений на языке Clojure. Здесь не будет сложной логики и модных фреймворков. Будет использоваться ряд библиотек для работы с примитивами. По мере упоминания я попробую в двух словах объяснить, какой функционал они предоставляют.

Архитектура веб-приложений в примитивах состоит из веб-сервера, который направляет запросы на обработчики в зависимости от пути, параметров, метода. Обработчик выполняет определенный код, делает запросы к базе данных, работает с файловой системой. После обработки запроса, генерируется ответ и отсылается клиенту.

Наше приложение будет принимать через форму одно значение, брать из базы данных второе, складывать их, а результат отдавать клиенту. При этом введенное значение будет заменять старое в базе данных. Глупая, бесполезная и не интересная логика — я знаю.

Для разработки приложения на Clojure нам понадобится, разумеется, Clojure и ряд вспомогательных библиотек. Первым делом качаем Cloj, ВНЕЗАПНО, Leiningen. Действительно, чтобы писать на Clojure, можно не устанавливать сразу Clojure, а скачать Leiningen. Это утилита для сборки проектов на 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.

Вот как выглядит работа нашего приложения:

image
image
image
image
image
image

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"})))))

Теперь наше приложение корректно обрабатывает неподходящие данные. В случае ошибки, клиенту будет показано сообщение.
image

Заключение


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

Articles