...

среда, 14 мая 2014 г.

Как я написал web-приложение, используя только clojure


Недавно я познакомился с интересным языком — clojure. Мне сразу понравились ленивые и иммутабельные коллекции, stm, макросы, обилие скобочек и dsl на все случаи жизни.

И я решил попробовать сделать web-приложение, используя только clojure.


Приложение




Было задумано создать простую искалку субтитров, которая:


  • каждые 5 минут индексирует новые субтитры на addicted, notabenoid и других сервисах;

  • имеет одностраничный web-интерфейс с поиском без перезагрузки страницы;

  • показывает в web-интерфейсе количество проиндексированных субтитров и меняет его при появлении новых;

  • имеет простое api для взаимодействия с десктопным клиентом.




Парсеры




На удивление парсеры было писать просто и удобно. Сначала казалось, что скобочек уж очень много, но threading макросы (->, ->>, -<> и -<>> — передача результата аргументом следующему выражению) очень помогали.

Например, кусок парсера notabenoid, делающий одно и тоже на python и clojure:















clojurepython


(defn get-release-page-result
"Get release page result"
[page]
(-<>> (get-release-page-url page)
helpers/fetch
(html/select <> [:ul.search-results :li :p :a])
(map (helpers/make-safe book-from-line nil))
(remove nil?)
(map episodes-from-book)
flatten))



def get_release_page_result(page):
"""Get release page result"""
url = get_release_page_url(page)
content = requests.get(url).content
soup = BeautifulSoup(content)
for line in get_lines_from_soup(soup):
book = get_book_from_line(line)
if book:
yield from get_episodes_from_book(book)

16 скобочек14 скобочек

Для запуска парсеров используется библиотека at-at, для парсинга html — enlive. Результат записывается в elasticsearch.

Серверная часть




Сервер



Как сервер я выбрал 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))))))




И даже размер “скомпилированного” файла оказался не таким уж большим — всего 290кб.

Как огромный плюс использования clojure вместе с clojurescript — можно писать один код для клиента и сервера при помощи cljx.


Выводы




Хоть clojure и позволяет разрабатывать web-приложения без знания и использования html, css и javascript, но продакшен-проекты я бы так делать не решился.

Исходный код результата.

Сам результат.


This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


Комментариев нет:

Отправить комментарий