Веб-приложение на Clojure. Часть 2

от автора

Здравствуйте, уважаемые пользователи и посетители Хабра. В первой статье Веб-приложения на Clojure были рассмотрены базовые инструменты и библиотеки для построения веб-проектов на Clojure. А именно Leiningen, Ring, Compojure, Monger и Selmer. Здесь же речь пойдет об их практическом применении.

Тем кто настроен на саморазвитие, через постижение кода в обход чтения статьи — прошу в конец страницы за ссылкой проекта на Github.

Введение

И так начнем по порядку. Чтобы вам было интереснее, я решил выбрать более-менее прикладную направленность статьи. Сегодня мы создадим простое веб-приложение на Clojure, сие будет управлять заметками. Предполагаю, что у вас уже установлены Leiningen, Clojure и MongoDB (не забудьте его включить). Львиная доля содержания находится непосредственно в комментариях в коде, который для вашего удобства скрыт в спойлеры.


IDE

Для Clojure есть много разных редакторов и IDE, в этой статье я не стану приводить их плюсы и минусы, пуще вообще некогда не стану. У всех разные предпочтения, что использовать решать только вам. Я использую LightTable который написан на ClojureScript и полностью им доволен, для него имеется большое количество модулей, из коробки он располагает всем необходимым для начала разработки на Clojure и ClojureScript, в нем присутствует модуль ParEdit. Вам ничего не придется настраивать для подключения к проекту удаленно или локально по repl. Взаимодействие с repl в LightTable весьма своеобразно, на мой субъективный взгляд очень удобно — вы можете вызывать функции и просматривать их результаты в отдельном окне в режиме live (как например в Emacs и во всех других IDE) или делать тоже самое непосредственно в коде, достаточно перевести курсор на первую или последнюю скобку выражения и нажать cmd + enter (MacOS), после этого LightTable создаст соединение repl и скомпилирует это выражение, вам остается ввести название скомпилированной функции или переменной строкой ниже и просмотреть его результат прямо в коде.


Back-end

Project

Первым делом создадим наш проект: $ lein new compojure notes

Теперь у нас есть каталог с заготовкой нашего приложения. Давайте перейдем в него и откроем в редакторе файл project.clj. Необходимо добавить в него зависимости от используемых нами библиотек:

project.clj

(defproject notes "0.1.0-SNAPSHOT"   :description "Менеджер заметок"   :min-lein-version "2.0.0"   :dependencies [; Да-да, сам Clojure тоже подключаем                  ; как зависимость                  [org.clojure/clojure "1.6.0"]                   ; Маршруты для GET и POST запросов                  [compojure "1.3.1"]                   ; Обертка (middleware) для наших                  ; маршрутов                  [ring/ring-defaults "0.1.5"]                   ; Шаблонизатор                  [selmer "0.8.2"]                   ; Добавляем Monger                  [com.novemberain/monger "2.0.1"]                   ; Дата и время                  [clojure.joda-time "0.6.0"]]    ; Поскольку веб-сервер подключать мы будем в   ; следующей статье, пока доверим это дело   ; Ring'у в который включен свой веб-сервер Jetty   :plugins [[lein-ring "0.8.13"]]    ; При запуске приложения Ring будет   ; использовать переменную app содержащую   ; маршруты и все функции которые они содержат   :ring {:handler notes.handler/app}   :profiles {:dev              {:dependencies               [[javax.servlet/servlet-api "2.5"]                [ring-mock "0.1.5"]]}}) 

При запуске проекта Leiningen установит все указанные зависимости автоматически, все что вам необходимо так это указать их в project.clj и быть подключенными к интернету. Далее перейдем к созданию серверной части нашего приложения. Логичнее разместить функции отображения, работы с БД, обработчиков и маршрутов по отдельным файлам, чтобы не было конфликтов имен и brain-fucking’a неудобств, но это кому как нравится.


Handler (главный обработчик)

Начнем с handler.clj, о его создании за нас уже позаботился Lein и он находится в каталоге /src/notes (в нем так же находится весь наш Clojure код). Это важная часть нашего приложения в ней содержится переменная app, которая включает в себя маршруты нашего приложения и базовый middleware (обертка запросов и ответов) для HTTP. Добавим в пространство имен маршруты нашего приложения, получится следующий код:

handler.clj

(ns notes.handler   (:require     ; Маршруты приложения    [notes.routes :refer [notes-routes]]     ; Стандартные настройки middleware    [ring.middleware.defaults :refer [wrap-defaults site-defaults]]))  ; Обернем маршруты в middleware (def app   (wrap-defaults notes-routes site-defaults)) 


Routes (маршруты)

Теперь создадим файл routes.clj, в нем разместим наши маршруты — которые при запросах методами GET и POST по указанным URI будут вызывать функции обработчиков форм, и отображений страниц. В этой части приложения мы используем API Compojure. Сразу приношу извинения за огромные куски кода тем кого это смущает, но в них я добавил множество комментариев, чтобы вам было легче понять логику их работы:

routes.clj

(ns notes.routes   (:require     ; Работа с маршрутами    [compojure.core :refer [defroutes GET POST]]    [compojure.route :as route]     ; Контроллеры запросов    [notes.controllers :as c]     ; Отображение страниц    [notes.views :as v]     ; Функции для взаимодействия с БД    [notes.db :as db]))  ; Объявляем маршруты (defroutes notes-routes    ; Страница просмотра заметки   (GET "/note/:id"        [id]         ; Получим нашу заметку по ее ObjectId        ; и передадим данные в отображение        (let [note (db/get-note id)]          (v/note note)))    ; Контроллер удаления заметки по ее ObjectId   (GET "/delete/:id"        [id]        (c/delete id))    ; Обработчик редактирования заметки   (POST "/edit/:id"         request         (-> c/edit))    ; Страница редактирования заметки   ; на деле, полагаю использовать   ; ObjectId документа в запросах   ; плохая идея, но в качестве   ; примера сойдет.   (GET "/edit/:id"        [id]         ; Получим нашу заметку по ее ObjectId        ; и передадим данные в отображение        (let [note (db/get-note id)]          (v/edit note)))    ; Обработчик добавления заметки   (POST "/create"          ; Можно получить необходимые нам значения         ; в виде [title text], но мы возьмем         ; request полностью и положим         ; эту работу на наш обработчик         request          ; Этот синтаксический сахар аналогичен         ; выражению: (create-controller request)         (-> c/create))    ; Страница добавления заметки   (GET "/create"        []        (v/create))    ; Главная страница приложения   (GET "/"        []         ; Получим список заметок и        ; передадим его в fn отображения        (let [notes (db/get-notes)]          (v/index notes)))    ; Ошибка 404   (route/not-found "Ничего не найдено")) 


Controllers (обработка форм)

Для обработки POST, иногда GET запросов, в маршрутах выше мы используем так называемые функции «контроллеры», вынесем их в отдельный файл. Здесь я намеренно опускаю полноценную проверку валидности входных данных так как это заслуживает отдельной статьи. Имя этому файлу controllers.clj, содержание его следующее:

controllers.clj

(ns notes.controllers   (:require     ; Функция редиректа    [ring.util.response :refer [redirect]]     ; Функции для взаимодействия с БД    [notes.db :as db]))  (defn delete   "Контроллер удаления заметки"   [id]   (do     (db/remove-note id)     (redirect "/")))  (defn edit   "Контроллер редактирования заметки"   [request]    ; Получаем данные из формы   (let [note-id (get-in request [:form-params "id"])         note {:title (get-in request [:form-params "title"])               :text (get-in request [:form-params "text"])}]      ; Проверим данные     (if (and (not-empty (:title note))              (not-empty (:text note)))        ; Если все ОК       ; обновляем документ в БД       ; переносим пользователя       ; на главную страницу       (do         (db/update-note note-id note)         (redirect "/"))        ; Если данные пусты тогда ошибка       "Проверьте правильность введенных данных")))  (defn create   "Контроллер создания заметки"   [request]    ; Получаем данные из формы   ; не будем плодить переменные   ; и сразу создадим hash-map   ; (ассоциативный массив)   (let [note {:title (get-in request [:form-params "title"])               :text (get-in request [:form-params "text"])}]      ; Проверим данные     (if (and (not-empty (:title note))              (not-empty (:text note)))        ; Если все ОК       ; добавляем их в БД       ; перенесем пользователя       ; на главную страницу       (do         (db/create-note note)         (redirect "/"))        ; Если данные пусты тогда ошибка       "Проверьте правильность введенных данных"))) 


DB (взаимодействие с MongoDB)

Очень интересная часть приложения, в её построении нам поможет библиотека Monger. Создадим файл db.clj, в нем будут хранится функции для взаимодействия с MongoDB. Конечно мы можем вызывать функции Monger напрямую в маршрутах и контролерах, но за это мы получим возмездие в отладке, расширении вместе с кучей дублирующегося кода, тем самым приумножим конечное кол-во строк кода. Monger так-же позволяет делать запросы к MongoDB посредством DSL запросов (для реляционных СУБД есть отличная библиотека sqlcorma), это очень удобно для сложных запросов, но в этой статье я не буду их описывать. Давайте добавим функции в db.clj:

db.clj

(ns notes.db   (:require     ; Непосредственно Monger    monger.joda-time ; для добавления времени и даты    [monger.core :as mg]    [monger.collection :as m]    [monger.operators :refer :all]     ; Время и дата    [joda-time :as t])    ; Импортируем методы из Java библиотек   (:import org.bson.types.ObjectId            org.joda.time.DateTimeZone))  ; Во избежание ошибок нужно указать часовой пояс (DateTimeZone/setDefault DateTimeZone/UTC)  ; Создадим переменную соединения с БД (defonce db   (let [uri "mongodb://127.0.0.1/notes_db"         {:keys [db]} (mg/connect-via-uri uri)]     db))  ; Приватная функция создания штампа даты и времени (defn- date-time   "Текущие дата и время"   []   (t/date-time))  (defn remove-note   "Удалить заметку по ее ObjectId"   [id]    ; Переформатируем строку в ObjectId   (let [id (ObjectId. id)]     (m/remove-by-id db "notes" id)))  (defn update-note   "Обновить заметку по ее ObjectId"   [id note]    ; Переформатируем строку в ObjectId   (let [id (ObjectId. id)]      ; Здесь мы используем оператор $set     ; с его помощью если в документе имеются     ; другие поля они не будут удалены     ; обновятся только те которые есть     ; в нашем hash-map + если он включает     ; поля которых нет в документе они     ; они будут добавлены к нему.     ; Так-же обновлять документы можно     ; по их ObjectId с помощью     ; функции update-by-id,     ; для наглядности я оставил обновление     ; по любым параметрам     (m/update db "notes" {:id id}                ; Обновим помимо документа               ; дату его создания               {$set (assoc note                       :created (date-time))})))  (defn get-note   "Получить заметку по ее ObjectId"   [id]    ; Если искать документ по его :_id   ; и в качестве значения передать   ; ему строку а не ObjectId   ; мы получим ошибку, поэтому   ; переформатируем его в тип ObjectId   (let [id (ObjectId. id)]      ; Эта функция вернет hash-map найденного документа     (m/find-map-by-id db "notes" id)))  (defn get-notes   "Получить все заметки"   []    ; Find-maps возвращает все документы   ; из коллеции в виде hash-map   (m/find-maps db "notes"))  (defn create-note   "Создать заметку в БД"    ; Наша заметка принимается от котроллера   ; и имеет тип hash-map c видом:   ; {:title "Заголовок" :text "Содержание"}   [note]    ; Monger может сам создать ObjectId   ; но разработчиками настоятельно рекомендуется   ; добавить это поле самостоятельно   (let [object-id (ObjectId.)]      ; Нам остается просто передать hash-map     ; функции создания документа, только     ; добавим в него сгенерированный ObjectId     ; и штамп даты и времени создания     (m/insert db "notes" (assoc note                            :_id object-id                            :created (date-time))))) 


Views (представление HTML шаблонов)

В файле views.clj мы разместим функции отображающие HTML шаблоны и передающие в них данные. В этом деле нам поможет библиотека Selmer, вдохновленная системой представления данных в шаблонах Django. Так же Selmer позволяет добавлять фильтры (функции) для обработки данных в самом шаблоне, тэги и предоставляет гибкие настройки самого себя. Займемся написанием функций отображения страниц:

views.clj

(ns notes.views   (:require     ; "Шаблонизатор"    [selmer.parser :as parser]    [selmer.filters :as filters]     ; Время и дата    [joda-time :as t]     ; Для HTTP заголовков    [ring.util.response :refer [content-type response]]     ; Для CSRF защиты    [ring.util.anti-forgery :refer [anti-forgery-field]]))  ; Подскажем Selmer где искать наши шаблоны (parser/set-resource-path! (clojure.java.io/resource "templates"))  ; Чтобы привести дату в человеко-понятный формат (defn format-date-and-time   "Отформатировать дату и время"   [date]   (let [formatter (t/formatter "yyyy-MM-dd в H:m:s" :date-time)]     (when date       (t/print formatter date))))  ; Добавим фильтр для использования в шаблоне (filters/add-filter! :format-datetime                      (fn [content]                        [:safe (format-date-and-time content)]))  ; Добавим тэг с полем для форм в нем будет находится ; автоматически созданное поле с anti-forgery ключом (parser/add-tag! :csrf-field (fn [_ _] (anti-forgery-field)))  (defn render [template & [params]]   "Эта функция будет отображать наши html шаблоны   и передавать в них данные"   (-> template       (parser/render-file          ; Добавим к получаемым данным постоянные         ; значения которые хотели бы получать         ; на любой странице         (assoc params           :title "Менеджер заметок"           :page (str template)))        ; Из всего этого сделаем HTTP ответ       response       (content-type "text/html; charset=utf-8")))  (defn note   "Страница просмотра заметки"   [note]   (render "note.html"            ; Передаем данные в шаблон           {:note note}))  (defn edit   "Страница редактирования заметки"   [note]   (render "edit.html"            ; Передаем данные в шаблон           {:note note}))  (defn create   "Страница создания заметки"   []   (render "create.html"))  (defn index   "Главная страница приложения. Список заметок"   [notes]   (render "index.html"            ; Передаем данные в шаблон           ; Если notes пуст вернуть false           {:notes (if (not-empty notes)                     notes false)})) 


Front-end

HTML шаблоны

Наше приложение почти готово, осталось создать HTML файлы в которых будут отображаться данные. Каталог /resources необходим для размещения статических файлов, т.е. даже после компиляции приложения в .jar файл мы сможем заменять в нем файлы. В public в следующей статье мы добавим CSS таблицы. Ну а пока создадим каталог templates где расположим HTML файлы.


Первым делом создадим базовый файл для всех шаблонов, в нем будет содержаться основная разметка для всех страниц и блок content в котором будет размещаться разметка остальных разделов. Начнем:

base.html

<!DOCTYPE html> <html> <head>      <META http-equiv="Content-Type" content="text/html; charset=UTF-8">     <title>{{title}}</title>  </head> <body>      <ul>         <li>             {% ifequal page "index.html" %}             <strong>Все заметки</strong>             {% else %}             <a href="/">Все заметки</a>             {% endifequal %}         </li>         <li>             {% ifequal page "create.html" %}             <strong>Добавить заметку</strong>             {% else %}             <a href="/create">Добавить заметку</a>             {% endifequal %}         </li>     </ul>      {% block content %}     {% endblock %}  </body> </html> 


Теперь сверстаем шаблон с формой создания заметки. В него, как и во все формы нашего приложения необходимо добавить тэг {% csrf-field %}, который мы создали в view.clj, иначе при отправке формы мы получим ошибку Invalid anti-forgery token. Приступим:

create.html

{% extends "base.html" %} {% block content %}  <h1>Создать заметку</h1>  <form action="POST">      {% csrf-field %}      <p>         <label>Заголовок</label><br>         <input type="text" name="title" placeholder="Заголовок">     </p>      <p>         <label>Заметка</label><br>         <textarea name="text"></textarea>     </p>      <input type="submit" value="Создать">  </form>  {% endblock %} 


У нас уже есть маршрут, представление и обработчик редактирования заметки, давайте создам для них шаблон с формой:

edit.html

{% extends "base.html" %} {% block content %}  <h1>Редактировать заметку</h1>  <form method="POST">      {% csrf-field %}      <input type="hidden" name="id" value="{{note._id}}">      <p>         <label>Заголовок</label><br>         <input type="text" name="title" value="{{note.title}}">     </p>      <p>         <label>Заметка</label><br>         <textarea name="text">{{note.text}}</textarea>     </p>      <input type="submit" value="Сохранить">      </form>  {% endblock %} 


Далее сверстаем шаблон просмотра заметки, в нем внимательный читатель в теге увидит странное представление данных, это нечто иное как наш фильтр созданный в view.clj. Для вызова фильтров мы используем такое выражение {{переменная|фильтр}}. Теперь сам код:

note.html

{% extends "base.html" %} {% block content %}  <h1>{{note.title}}</h1> <small>{{note.created|format-datetime}}</small>  <p>{{note.text}}</p>  {% endblock %} 


И наконец наша главная страница, где будет отображаться список заметок:

note.html

{% extends "base.html" %} {% block content %}  <h1>Заметки</h1>  {% if notes %} <ul>      {% for note in notes %}     <li>         <h4><a href="/note/{{note._id}}">{{note.title}}</a></h4>         <small>{{note.created|format-datetime}}</small>         <hr>         <a href="/edit/{{note._id}}">Редактировать</a> | <a href="/delete/{{note._id}}">Удалить</a>     </li>     {% endfor %}  </ul> {% else %} <strong>Заметок еще нет</strong> {% endif %}  {% endblock %} 


Заключение

Теперь наше приложение готово, конечно мы пропустили очень важный шаг — тестирование функций в repl, но в следующей статье мы остановимся на нем подробно.

Давайте запускать: $ lein ring server

Эта команда как и lein run установит все зависимости, запустит наше приложение на базовом веб-сервере Ring’a Jetty по адресу localhost:3000. Замечу, что пока мы не можем cкомпилировать наше приложение в .jar или .war файл или запускать его через lein run.


Дополнительные ссылки

В следующей статье мы добавим к нашему веб-приложению веб-сервер immutant, и реализуем авто-обновление кода «на лету» т.е. по мере сохранения файлов в редакторе наш сервер будет автоматически обновлять их и мы сможем видеть результаты изменения кода после перезагрузки страницы а не сервера как сейчас. В данный момент «на лету» мы можем изменять лишь HTML файлы. А так-же добавим к нашему HTML Bootstrap классы, так как выглядит верстка очень не весело, несмотря на то, что суть статей заключается не в красивом оформлении, полагаю не стоит возвращаться в WEB 1.0.

Несмотря на пристальную проверку перед публикацией статьи, я буду благодарен вам если заметите неточности в описании или ошибки в грамматике и дадите мне знать об этом в сообщениях. Так-же хотелось бы поблагодарить людей проявивших интерес к моей первой статье, с радостью продолжаю рассказывать вам о веб-разработке на Clojure. На этом я с вами прощаюсь и желаю всего лучшего!

ссылка на оригинал статьи http://habrahabr.ru/post/263131/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *