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

от автора

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

Приложение

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

  • каждые 5 минут индексирует новые субтитры на addicted, notabenoid и других сервисах;
  • имеет одностраничный web-интерфейс с поиском без перезагрузки страницы;
  • показывает в web-интерфейсе количество проиндексированных субтитров и меняет его при появлении новых;
  • имеет простое api для взаимодействия с десктопным клиентом.

Парсеры

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

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

clojure python
(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, но продакшен-проекты я бы так делать не решился.

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

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


Комментарии

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

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