Основы Clojure Web Applications

    Сегодня я попробую показать основы создания веб приложений на языке 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

    Заключение


    Если я плохо осветил какую-либо часть, просьба сообщить мне об этом — статью дополню. В случае вопросов, просьба не стесняться. Здоровая и не очень критика также приветствуется.
    Исходные коды этого примера можно найти в репозитории.
    • +16
    • 8,1k
    • 6
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 6
    • 0
      Вообще здорово, статья понравилась. Конечно, я понимаю, что у вас какой-то опыт уже есть, и поэтому вы решили описать минимальную конфигурацию проекта, чтобы тот, кто следовал вашей инструкции, не наступал на ваши грабли, а дал Лейнингену сделать всю грязную работу. Но эффект внезапности оказался весьма сильным :) Получилось так: вот вам минимальная конфигурация, вот код, а вот очень сжатое объяснение по делу, что и где происходит. Пришлось статью потом еще раз внимательнее перечитать, чтобы заметить все детали.

      Добавлю, что начинать с Clojure немного тяжеловато, особенно тем, кто приходит с традиционных языков (Java или C#) и привык к тому, что есть среда разработки, которая и подскажет где надо, и ошибки поотмечает в коде, и соломки подстелит, где надо, чтоб падать было помягче. Те, кто приходит с Лиспа или со скриптовых языков, как-то лучше подготовлены к тому, чтобы пользоваться редактором и вбивать команды в командной строке. Мне лично очень нравится LaClojure — плагин от JetBrains для своей IntelliJ. REPL, интеграция с Leiningen и какой-никакой автокомплит.

      И еще вопрос: в чем преимущества clojure для веб-разработки? На мой взгляд, все-таки основной козырь языка — конкурентное программирование. Я лично пока использую его по аналогии с rule-engines — когда система правил компилируется в jar-файл, который потом используется приложением как черный ящик. Тут то же самое, только вместо какого-то языка правил — Lisp, а в результате получается не обязательно какой-то решатель или экспертная система, а все, что угодно. Сейчас получается так, что большая часть системы продолжает писаться на Java, но если есть узкие места — асинхронный процессинг или какая-то особенно изощренная логика, в проект кидается clojure.jar и вперед.
      • 0
        ну так где где, а в вебе надо конкурентность кложура как раз
        • 0
          Конкурентность в вебе нужна на низком уровне. Современные фреймворки сделили все, чтобы программисту не было нужды знать, что там за магия творится.
      • –3
        На вкус и цвет, конечно, да и не сторонник я синтаксического сахара, но у Clojure он просто ужасен.
        • +2
          это Lisp, и он прекрасен!
        • 0
          Спасибо за статью, очень интересно. А можете рассказать, как работать с сессиями и печеньками?

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