Недавно я познакомился с интересным языком — clojure. Мне сразу понравились ленивые и иммутабельные коллекции, stm, макросы, обилие скобочек и dsl на все случаи жизни.
И я решил попробовать сделать web-приложение, используя только clojure.
Приложение
Было задумано создать простую искалку субтитров, которая:
- каждые 5 минут индексирует новые субтитры на addicted, notabenoid и других сервисах;
- имеет одностраничный web-интерфейс с поиском без перезагрузки страницы;
- показывает в web-интерфейсе количество проиндексированных субтитров и меняет его при появлении новых;
- имеет простое api для взаимодействия с десктопным клиентом.
Парсеры
На удивление парсеры было писать просто и удобно. Сначала казалось, что скобочек уж очень много, но threading макросы (->, ->>, -<> и -<>> — передача результата аргументом следующему выражению) очень помогали.
Например, кусок парсера notabenoid, делающий одно и тоже на python и clojure:
clojure | python |
|
|
16 скобочек | 14 скобочек |
Серверная часть
Сервер
Как сервер я выбрал http-kit, в основном из-за того, что мне захотелось web-сокетов. И их тут очень просто использовать, например, отправка всем клиентам количества проиндексированных субтитров после обновления будет выглядеть так:
(add-watch total-count :notifications
#(doseq [con @subscribers]
(send! con (prn-str {:total-count %4}))))
Роутинг
Для роутинга — compojure. Тут нет никаких отличий от django и других популярных фреймворков:
(defroutes main-routes
(GET "/" [] (views/index-page))
(GET "/api/list-languages/" {params :params} (api/list-languages params))
(GET "/notifications/" [] push/notifications)
(route/resources const/static-path))
API
Так как мы везде используем clojure, то наше api должно возвращать результат в родных структурах данных и в json (для десктопного клиента на python). Библиотеки, которая так может, я не нашёл (уже нашёл), поэтому пришлось немного повелосипедить и изобрести свой мини-dsl:
(defn- get-writer
"Get writer from params"
[params]
(if (= (:format params) "json")
json/write-str
prn-str))
(defmacro defapi
"Define api method"
[name doc args & body]
`(defn ~name ~args
((get-writer (first ~args))
~@body)))
И как простой пример использования:
(defapi list-languages
"List all available languages"
[params]
(models/list-languages))
View
Для рендеринга html я воспользовался специальным dsl — hiccup, шаблон с ним выглядит немного «марсианским»:
(defn index-page []
(html5 [:head
[:title "Subman - subtitle search service"]
[:body
[:h1 "Welcome to subman!"]]))
Стили
Для стилей в clojure тоже есть свой dsl — garden. Код с ним выглядит тоже странно:
(defstyles main
[:.search-input {:z-index 100
:background-color "#fff"}]
[:.info-box {:text-align "center"
:font-size (px 18)}]
[:.search-result-holder {:padding-left 0
:padding-right 0}])
Клиентская часть
Клиентскую часть я писал не совсем на clojure, а на clojurescript, который в итоге компилируется в javascript. Как фреймворк я использовал reagent — биндинг к react.js для clojure, непроверяющий каждую секунду объекты на изменения (благодаря atom'ам) и использующий hiccup-подобный dsl для описания компонентов:
(defn info-box
"Show info box"
[text]
[:div.container.col-xs-12.info-box
[:h2 text]])
Тут всё очень даже хорошо, пока не нужно напрямую работать с js-библиотеками. Например, код для подключения typeahead к полю поиска:
(defn init-autocomplete
"Initiale autocomplete"
[query langs sources]
(let [input ($ "#search-input")]
(.typeahead input
(js-obj "highlight" true)
(js-obj "source"
(fn [query cb]
(cb (apply array
(take const/autocomplete-limit
(map #(js-obj "value" %)
(get-completion query
@langs
@sources))))))))
(.on input "typeahead:closed" (fn []
(reset! query (.val input))))))
UPD: После небольшого рефакторинга код стал менее страшным:
(defn completion-source
"Source for typeahead autocompletion"
[langs sources query cb]
(cb (->> (get-completion query
@langs
@sources)
(map #(js-obj "value" %))
(take const/autocomplete-limit)
(apply array))))
(defn init-autocomplete
"Initiale autocomplete"
[query langs sources]
(let [input ($ "#search-input")]
(.typeahead input
#js {:highlight true}
#js {:source (partial completion-source
langs sources)})
(.on input "typeahead:closed"
#(reset! query (.val input)))))
И даже размер “скомпилированного” файла оказался не таким уж большим — всего 290кб.
Как огромный плюс использования clojure вместе с clojurescript — можно писать один код для клиента и сервера при помощи cljx.
Выводы
Хоть clojure и позволяет разрабатывать web-приложения без знания и использования html, css и javascript, но продакшен-проекты я бы так делать не решился.
Исходный код результата.
Сам результат.