В этой статье я расскажу об одном из способов написать web-приложение на Clojure. Как человеку, сильно избалованному фреймворком Ruby on Rails, естественно мне хотелось что-то похожее и на Clojure. Рельсы отличают от других фреймворков очень большая пристрастность в выборе технологий. Например, это обязательное использование паттерна MVC, соглашений вместо конфигурации, принципа DRY (Don't Repeat Yourself) и т. д. Именно это мне и хотелось реализовать на Clojure, т. к. считаю этот язык инструментом следующего поколения даже по сравнению с Ruby, не говоря уже о Java или C#.
В Clojure пока нет устоявшегося стека для web-разработки. Каждый волен выбирать сам любые библиотеки и разрабатывать сайт в любой их комбинации. Я использовал следующие технологии.
- View: clj-soy 0.1.0 (обертка над google closure templates).
- Controller: ring 0.3.8 (обертка над сервером приложений Jetty), compojure 0.6.2 (DSL для описания маршрутов).
- Model: mysql-connector-java 5.1.6, clojure.contrib.sql.
Весь код проекта находится здесь: Архив проекта. Запускать его так:
$ lein deps
$ lein ring server.
Итак, приступим к созданию простого демонстрационного блога.
$ lein new www-test.
Мой project.clj выглядит так:
(defproject www-test "1.0.0-SNAPSHOT" :description "Clojure web-site demo" :dependencies [[org.clojure/clojure "1.2.1"] [compojure "0.6.2"] [clj-soy "0.1.0"] [mysql/mysql-connector-java "5.1.6"] [ring "0.3.8"]] :dev-dependencies [[swank-clojure "1.2.1"] [lein-ring "0.4.0"]] :ring {:handler www-test.system.routes/app} :resources-path "resources")
Здесь параметр ring указывает на пользовательскую функцию, устанавливающую маршруты (см. ниже). Параметр :resources-path устанавливает название дирректории, где находится дирректория public, в которой находятся общие ресурсы приложения (CSS, JavaScript, картинки).
Вьюхи я реализовал на google closure templates. Библиотека clj-soy очень хорошо оборачивает эту функциональность. Учитывая, в скольких проектах google используется эта библиотека, а также, сколько по ней документации, работать с google closure templates – одно удовольствие. Пример шаблона для формы редактирования:
{namespace posts} /** * Edit a post. * @param post The post. */ {template .edit} <html> <body> <h1>Edit a post</h1> <br/> <form action="/posts/update" method="post"> <input type="hidden" name="id" id="id" value="{$post.id}"/> Title: <input type="text" name="title" id="title" value="{$post.title}"/><br/> Text: <textarea id="body" name="body" rows="10" cols="30"> {$post.body} </textarea> <br/> <input type="submit" value="Update" /> </form> <br/> <a href="/posts">Back</a> </html> {/template}
Файл контроллера c_post.clj выглядит так (название с префиксом c_ выбрал только из удобства работы в Emacs; файлы с разными названиями проще находить через IDO):
(ns www-test.controllers.c-post (:use www-test.system.utils) (:require [www-test.models.m-post :as post] [clj-soy.template :as soy] [clojure.contrib.sql :as sql])) (defn index [] (render-action posts#index {:posts (post/fetch-list)})) (defn new [] (render-action posts#new {})) (defn create [post] (post/create post) (redirect-to "/posts")) (defn show [id] (render-action posts#show {:post (post/fetch id)})) (defn edit [id] (render-action posts#edit {:post (post/fetch id)})) (defn update [post] (post/update post) (redirect-to (str "/posts/show/" (:id post)))) (defn delete [id] (post/delete id) (redirect-to "/posts"))
Сделаю несколько пояснений по исходнику. Функции index, new, show, edit только отдают html-представление формы и не производят никаких побочных эффектов. Функции create, update и delete выполняют указанные операции над сущностями и редиректят на одну из страниц блога.
Модель m_post.clj я связал с алиасом post. Таким образом, когда нужно обратиться к базе данных, в контроллере вызов выглядит, например, так: (post/fetch-list), или так (post/update post), если нужно передать параметры. Я передаю все параметры в виде map-а, ключи которого – названия колонок в базе данных.
Рендеринг html-формы я сделал в стиле «соглашение вместо конфигурации». Оператор render-action – это макрос, который принимает на вход название экшена в виде entity#action, а также список параметров для clj-soy. Макрос описан в файле utils.clj и выглядит так.
(defmacro render-action [action params] `(let [splitted# (.split ~(str action) "#") entities# (first splitted#) action# (second splitted#)] (render-rest-page entities# action# ~params)))
Как видите, он разбирает параметр action и вызывает render-rest-page:
(defn render [template-file template-ns params] (let [tpl (soy/build template-file)] (soy/render tpl template-ns params))) (defn decorate-page [template-file template-ns params app-template-file app-template-ns] (let [content (render template-file template-ns params)] (render app-template-file app-template-ns {:content content}))) (defn render-rest-page [entities action params] (decorate-page (str *templates* "/" entities "/" action ".soy") (str entities "." action) params (str *templates* "/app/" entities ".soy") (str "app." entities)))
Функция decorate-page сначала рендерит указанную форму, затем врендеривает ее в общий шаблон приложения, добавляя стили, картинки и т. д. Мой шаблон posts.soy выглядит так:
{namespace app} /** * List of posts. * @param content Content */ {template .posts autoescape="false"} <Масса разного укращающего html-кода> {$content} <Окончание html> {/template}
Модель m_post.clj выглядит так:
(ns www-test.models.m-post (:use www-test.system.db www-test.system.utils) (:require [clojure.contrib.sql :as sql])) (defn fetch-list [] (doall (map struct-map->soy (doall (sql/with-connection *db* (sql/with-query-results posts ["SELECT * FROM post order by id desc"] (doall posts))))))) (defn create [post] (sql/with-connection *db* (sql/insert-values "post" ["title" "body"] [(:title post) (:text post)]))) (defn fetch [id] (struct-map->soy (sql/with-connection *db* (sql/with-query-results posts [(str "SELECT * FROM post where id = " id)] (first (doall posts)))))) (defn update [post] (sql/with-connection *db* (sql/update-values "post" [(str "id=" (:id post))] post))) (defn delete [id] (sql/with-connection *db* (sql/delete-rows "post" [(str "id=" id)])))
Здесь все достаточно просто за исключением функции struct-map->soy. Дело в том, что clj-soy не принимает идиоматичные map-ы в виде {:key value}. Ключ у него должен быть не ключевым словом, а строкой. Поэтому функция struct-map->soy просто переделывает ключи в строки.
(defn map->soy [m] (let [k (keys m) ks (map #(subs (str %) 1) k) ms (reduce (fn [m v] (assoc m (first v) (second v))) {} (map (fn [v1 v2] [v1 v2]) k ks)) rm (clojure.set/rename-keys m ms)] rm)) (defn struct-map->soy [sm] (let [m (reduce (fn [m k] (assoc m k (k sm))) {} (keys sm))] (map->soy m)))
Файл routes.clj выглядит так:
(ns www-test.system.routes (:use compojure.core, ring.adapter.jetty, ring.util.response, www-test.system.utils) (:require [www-test.controllers.c-post :as post] [compojure.route :as route] [compojure.handler :as handler] [clj-soy.template :as soy] [clojure.contrib.sql :as sql])) (defroutes main-routes (GET "/" [] (redirect-to "/posts")) (GET "/posts" [] (post/index)) (GET "/posts/new" [] (post/new)) (POST "/posts/create" {post :params} (post/create post)) (GET "/posts/show/:id" [id] (post/show id)) (GET "/posts/edit/:id" [id] (post/edit id)) (POST "/posts/update" {post :params} [post] (post/update post)) (GET "/posts/delete/:id" [id] (post/delete id)) (route/resources "/") (route/not-found "Page not found")) (def app (handler/site main-routes))
Из неописанных функций здесь только redirect-to:
(defn redirect-to [addr] (ring.util.response/redirect addr))
И, наконец, сама БД. Она содержит единственную таблицу:
CREATE TABLE post (
id int(11) NOT NULL AUTO_INCREMENT,
title varchar(512) DEFAULT NULL,
body text,
PRIMARY KEY (id))
Константа *db* описана в файле db.clj:
(ns www-test.system.db) (def *db* {:classname "com.mysql.jdbc.Driver" :subprotocol "mysql" :subname "//127.0.0.1:3306/wwwtest1" :user "myuser" :password "mypass"})
Вот, в общем, и все. Конечно, это просто демонстрационное приложение, и много еще чего предстоит сделать. Но, возможно, оно кому-то покажется интересным и подтолкнет к разработке собственных сайтов на Clojure.
Этот комментарий был удален автором.
ОтветитьУдалитьКстати, насчет веб-фреймворков: посмотрите на https://github.com/dnaumov/jaco. Правда, он еще в начальной (очень начальной :)) стадии разработки
ОтветитьУдалитьПосмотрел фреймворк jaco. Мне сложно его оценить, поскольку нету примеров его использования, а по тестам и исходникам вникнуть в него сложновато. То, в чем я разобрался, мне понравилось: архитектура MVC, CRUD, заготовки для основных форм, валидаторы. Есть и отличия в наших подходах.
ОтветитьУдалить1. Я считаю, что html-формы лучше всего писать на html. Просто обычно этим занимается дизайнер, а перегонять его поток сознания в hiccup -- для меня это слишком.
2. На мой взгляд, лучший способ метапрограммирования под web -- это генерация кода, как это сделано в Rails и Django. Двигаюсь в этом направлении, хотя и медленно.
Ну и еще. Маршрутизатор у вас -- это что-то! Может я не совсем проникся идеей, но по-моему уж больно сложно))) Как-никак, Compojure как раз и занимается маршрутизацией, так почему бы сопоставление с образцом не оставить именно этой библиотеке?