Pull to refresh

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

Reading time4 min
Views26K


Недавно я познакомился с интересным языком — 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))))))

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

Исходный код результата.
Сам результат.
Tags:
Hubs:
Total votes 56: ↑52 and ↓4+48
Comments17

Articles