Здравствуйте, уважаемые пользователи и посетители Хабра. В первой статье Веб-приложения на 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. Необходимо добавить в него зависимости от используемых нами библиотек:
(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. Добавим в пространство имен маршруты нашего приложения, получится следующий код:
(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. Сразу приношу извинения за огромные куски кода тем кого это смущает, но в них я добавил множество комментариев, чтобы вам было легче понять логику их работы:
(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, содержание его следующее:
(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:
(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 позволяет добавлять фильтры (функции) для обработки данных в самом шаблоне, тэги и предоставляет гибкие настройки самого себя. Займемся написанием функций отображения страниц:
(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 в котором будет размещаться разметка остальных разделов. Начнем:
<!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. Приступим:
{% 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 %}
У нас уже есть маршрут, представление и обработчик редактирования заметки, давайте создам для них шаблон с формой:
{% 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. Для вызова фильтров мы используем такое выражение {{переменная|фильтр}}. Теперь сам код:
{% extends "base.html" %} {% block content %} <h1>{{note.title}}</h1> <small>{{note.created|format-datetime}}</small> <p>{{note.text}}</p> {% endblock %}
И наконец наша главная страница, где будет отображаться список заметок:
{% 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/
Добавить комментарий