Как я написал 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))))))
    

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

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

    Similar posts

    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 17

      +3
      По моему без знания html и css не обойтись. Соответствующие dsl только предоставляют более простой синтаксис, а семантику знать надо.
        +4
        Поэтому и красным =)
        +1
        А DSL я так понимаю компилируемые? Т.е. при ошибке в шаблоне проект не скомпилируется?
          0
          Да, и это не перестаёт радовать, помимо прочих плюшек DSL.
            0
            Сам радуюсь этому же на Scala (шаблонизатор Scalate).
            +1
            Да, макросы выполняются во время компиляции.
              0
              Не знаю насчет garden, но hiccup не проверяет на предмет ошибок. Т.е. если вместо div написать diiv — так оно и отрендерится. Ну и генерировать структуры данных для скармливания hiccup-у можно в рантайме, хотя используя макросы библиотека и пытается сделать прекомпиляцию, где это возможно.

              Сильно подозреваю, что с garden аналогично, не запихивать же постоянно в нее поддержку новых экспериментальных css-свойств и подобного.
              0
              Большое спасибо за статью, как раз интересовала инфраструктура для веб-проектов в Clojure. Буду пристально вглядываться в библиотеки, которые вы упомянули.
                +2
                Ещё в сторону luminus посмотрите =)
                +2
                Добавьте пожалуйста сортировку языков, а то русский найти проблема
                  0
                  Из-за множественных закрывающих скобок в конце где-то на подсознательном уровне код такого вида

                  (.on input "typeahead:closed" (fn []
                                                      (reset! query (.val input))))))
                  

                  когда на него посмотришь, как будто говорит «Ну что написал? Ну-ну :))))))»
                    +5
                    Спасибо за статью, приятно видеть все возрастающий интерес к Clojure. Я так понимаю, у вас была задача попробовать побольше всего разного что напридумывали в Clojure?

                    Вместо defapi сделали бы middleware и обернули свои API routes в него.

                    Вместо js-obj в новых clojurescript версиях удобно писать #js [1 2 3] или #js {:a 1 :b 2} чтобы получить нативные js-объекты.

                    Зачем нужен dsl для css не очень понятно — у вас css динамический разве?

                    Размер скомпилированного js-файла тоже хотелось бы увидеть в gzipped варианте и с advanced compilation. А то у меня например весь datascript в 39 кб умещается, а тут аж 290
                      +1
                      Я так понимаю, у вас была задача попробовать побольше всего разного что напридумывали в Clojure?
                      Да, хотел попробовать clojure для web'а.

                      Зачем нужен dsl для css не очень понятно — у вас css динамический разве?
                      Статический, garden использовал только для того, чтобы всё было на clojure, это как некий челенж был.

                      Размер скомпилированного js-файла тоже хотелось бы увидеть в gzipped варианте и с advanced compilation. А то у меня например весь datascript в 39 кб умещается, а тут аж 290
                      290 и так в advanced, но там внутри reagent, react и jayq.
                      gzipped — 80
                      0
                      А такой вопрос, чем компилировали clojurescript в js? Использовали там grunt, минификаторы, или ещё что-то?
                        0
                        Боюсь ошибиться, но leiningen с плагином для clojurescript-а используется обычно.
                          +1
                          leiningen с lein-cljsbuild
                            0
                            Clojurescript использует Google closure compiler для компиляции/минификации получившегося js.

                          Only users with full accounts can post comments. Log in, please.