Статьи

Clojure веб-разработка — состояние дел — часть 2

Это вторая часть моей серии «Clojure web development». Вы можете обсудить первую часть в этой теме Reddit. После прочтения комментариев я должен объяснить два предположения, которые я имел при написании этой серии:

  • Делайте вещи понятными для людей из-за пределов Clojure, особенно для разработчиков Java. Вот почему я использую REST / JSON в пользу транзита и компонента в качестве реализации «внедрения зависимости», что можно легко объяснить как эквивалент среды Spring. То же самое относится и к Om, который немного многословен, но, на мой взгляд, его легче понять с самого начала, и он более широк, чем другие обертки React.
  • Упрощайте загрузку на компьютере разработчика. Это практическое руководство, и все отдельные шаги были совершены в GitHub. Вот почему я использую MongoDB, которая не может быть лучшим выбором для масштабирования вашего приложения для миллионов пользователей, но идеально подходит для начальной загрузки — без схемы, таблиц, просто вставьте данные и начните работать. Я очень рекомендую беседу о постоянстве с Полглотом из Honza Kral, где он призывает начать с простого и оптимизировать для счастья разработчика в начале проекта.

В предыдущем посте мы загрузили базовое веб-приложение, обслуживающее данные REST, с (на данный момент статичным) интерфейсом Clojurescript, полностью перезагружаемым благодаря перезагруженным repl и figwheel. Вы можете найти окончательную рабочую версию в этой ветке .

Сегодня мы собираемся показать список контактов, хранящихся в MongoDB. Я предполагаю, что у вас установлен MongoDB, если нет — это тривиально с докером .

Обслуживающий список контактов из базы данных

Хорошо, давайте начнем. В бэкэнде нам нужно добавить некоторые зависимости в project.clj:

1
2
3
4
:dependencies
   ...
   [org.danielsz/system "0.1.9"]
   [com.novemberain/monger "2.0.0"]]

monger — это идиоматическая оболочка Clojure для Java-драйвера Mongo, а system — хороший набор компонентов для различных хранилищ данных, включая Mongo (немного похожий на Spring Data for Spring).

Для взаимодействия с хранилищем данных мне нравится использовать концепцию абстрактного хранилища. Это должно скрыть детали реализации от вызывающего и позволит в будущем переключиться на другой магазин. Итак, давайте создадим абстрактный интерфейс (в Clojure — протоколе) в components/repo.clj :

1
2
3
4
(ns modern-clj-web.component.repo)
 
(defprotocol ContactRepository
  (find-all [this]))

Это необходимо в качестве параметра, позволяющего среде выполнения Clojure отправлять правильную реализацию этого хранилища. Монго реализация с Monger действительно проста:

01
02
03
04
05
06
07
08
09
10
11
12
(ns modern-clj-web.component.repo
   (:require [monger.collection :as mc]
             [monger.json]))
...
 
(defrecord ContactRepoComponent [mongo]
  ContactRepository
  (find-all [this]
    (mc/find-maps (:db mongo) "contacts")))
 
(defn new-contact-repo-component []
  (->ContactRepoComponent {}))

Что следует отметить здесь:

  • mc/find-maps просто возвращает все записи из коллекции в виде карт Clojure
  • ContactComponent внедряется с компонентом mongo, созданным системной библиотекой, которая добавляет клиента Mongo под ключом :db
  • Поскольку этот компонент не имеет состояния, нам не нужно реализовывать интерфейс component/Lifecycle , но он все еще может быть подключен в системе, как типичный компонент, учитывающий жизненный цикл.
  • Требование monger.json добавляет поддержку сериализации JSON для типов Mongo (например, ObjectId )

Хорошо, теперь пришло время использовать наш новый компонент в конечной точке / example.clj:

1
2
3
4
5
6
7
8
(:require
  ...
  [modern-clj-web.component.repo :as r])
 
(defn example-endpoint [{repo :contact-repo}]
 (routes
...
   (GET "/contacts" [] (response (r/find-all repo)))

Нотация {repo :contact-repo} (деструктуризация) автоматически связывает ключ :contact-repo из системной карты со значением repo . Поэтому нам нужно назначить наш компонент этому ключу в system.clj :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
(:require
  ...
  [modern-clj-web.component.repo :refer [new-contact-repo-component]]
  [system.components.mongo :refer [new-mongo-db]])
 
 (-> (component/system-map
          :app  (handler-component (:app config))
          :http (jetty-server (:http config))
          :example (endpoint-component example-endpoint)
          :mongo (new-mongo-db (:mongo-uri config))
          :contact-repo (new-contact-repo-component))
         (component/system-using
          {:http [:app]
           :app  [:example]
           :example [:contact-repo]
           :contact-repo [:mongo]}))))

Вкратце — мы используем системный new-mongo-db для создания компонента Mongo, сделаем его зависимым от репозитория, который сам по себе является зависимостью примера конечной точки.

И, наконец, нам нужно настроить :mongo-uri конфигурации :mongo-uri в config.clj :

1
2
3
(def environ
  {:http {:port (some-> env :port Integer.)}}
   :mongo-uri "mongodb://localhost:27017/contacts"})

Чтобы проверить, работает ли он нормально, перезапустите repl, введите (go) раз и сделайте GET по адресу http: // localhost: 3000 / contacts .

1
2
curl http://localhost:3000/contacts
[]

Итак, мы получили пустой список, так как у нас нет данных в базе данных Mongo. Давайте добавим некоторые с консоли Монго:

1
2
3
4
5
mongo localhost:27017/contacts
MongoDB shell version: 2.4.9
connecting to: localhost:27017/contacts
>  db.contacts.insert({firstname: "Al", lastname: "Pacino"});
>  db.contacts.insert({firstname: "Johnny", lastname: "Depp"});

И, наконец, наша конечная точка должна вернуть эти две записи:

1
2
curl http://localhost:3000/contacts
[{"lastname":"Pacino","firstname":"Al","_id":"56158345fd2dabeddfb18799"},{"lastname":"Depp","firstname":"Johnny","_id":"56158355fd2dabeddfb1879a"}]

Милая! Опять же — в случае каких-либо проблем, проверьте этот коммит .

Получение контактов из ClojureScript

На этом шаге мы получим контакты с помощью вызова AJAX на нашем интерфейсе ClojureScript. Как обычно, для начала нам нужно немного зависимостей в project.clj :

1
2
3
4
5
:dependencies
     ...
       [org.clojure/clojurescript "1.7.48"]
       [org.clojure/core.async "0.1.346.0-17112a-alpha"]
       [cljs-http "0.1.37"]

ClojureScript уже должен быть виден при использовании figwheel , но всегда лучше явно figwheel конкретную версию. cljs-http является HTTP-клиентом для ClojureScript, а core.async предоставляет средства для асинхронной связи в модели CSP, особенно полезной в ClojureScript. Посмотрим, как это работает на практике.

Чтобы сделать AJAX-вызов, нам нужно вызвать методы из cljs-http.client , поэтому давайте добавим это в core.cljs :

1
2
3
4
(ns ^:figwheel-always modern-clj-web.core
  (:require [cljs-http.client :as http]))
 
(println (http/get "/contacts"))

Вы должны увидеть #object[cljs.core.async.impl.channels.ManyToManyChannel] . Что это за безумие ???

Это время, когда мы входим в core.async . Наиболее распространенный способ обработки асинхронных сетевых вызовов из Javascript — использование обратных вызовов или обещаний. Способ core.async заключается в использовании каналов. Это делает ваш код более похожим на последовательность синхронных вызовов, и его легче рассуждать. Таким образом, функция http/get возвращает канал, на котором публикуется результат при получении ответа. Чтобы получить это сообщение, нам нужно прочитать с этого канала, используя <! функция. Поскольку это блокировка, нам также нужно окружить этот вызов макросом go , как в языке go . Таким образом, правильный способ получения контактов выглядит так:

1
2
3
4
5
6
7
(:require
...
  [cljs.core.async :refer [<! >! chan]])
 
(go
  (let [response (<! (http/get "/contacts"))]
    (println (:body response))))

Добавление компонента Om

Работа с внешним кодом без какой-либо структуры может быстро превратиться в настоящий кошмар. В конце 2015 года у нас есть две основные платформы JS: Angular nad React. Парадигмы ClojureScript (функциональное программирование, неизменяемые структуры данных) прекрасно вписываются в философию React. Вкратце, приложение React состоит из компонентов, принимающих данные в качестве входных данных и отображающих HTML в качестве выходных данных. Вывод — это не реальный DOM, а так называемый виртуальный DOM , который помогает вычислять разность между текущим и обновленным представлениями.

Среди многих оболочек React в ClojureScript мне нравится использовать Om с ом-инструментами, чтобы уменьшить детализацию. Давайте введем это в наш project.clj :

1
2
3
4
:dependencies
     ...
   [org.omcljs/om "0.9.0"]
   [prismatic/om-tools "0.3.12"]

Чтобы отобразить компонент «hello world», нам нужно добавить код в core.cljs :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
(:require
   ...
            [om.core :as om]
            [om-tools.core :refer-macros [defcomponent]]
            [om-tools.dom :as dom :include-macros true]))
 
(def app-state (atom {:message "hello from om"}))
 
(defcomponent app [data owner]
  (render [_]
    (dom/div (:message data))))
 
(om/root app app-state
         {:target (.getElementById js/document "main")})

Что тут происходит? Основной концепцией Om является сохранение всего состояния приложения в одном глобальном атоме , что является способом управления состоянием Clojure. Таким образом, мы передаем эту карту app-state (обернутую в atom ) в качестве параметра в om/root который монтирует компоненты в реальный DOM ( <div id="main"/> из index.html ). Компонент app просто отображает значение :message , поэтому вы должны увидеть «hello from om». Если у вас запущен fighweel , вы можете изменить значение сообщения, и оно должно быть обновлено немедленно.

И, наконец, давайте представим наши контакты с Om:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
(defn get-contacts []
  (go
    (let [response (<! (http/get "/contacts"))]
      (:body response))))
 
(defcomponent contact-comp [contact _]
  (render [_]
    (dom/li (str (:firstname contact) " " (:lastname contact)))))
 
(defcomponent app [data _]
  (will-mount [_]
    (go
      (let [contacts (<! (get-contacts))]
        (om/update! data :contacts contacts))))
  (render [_]
    (dom/div
      (dom/h2 (:message data))
      (dom/ul
        (om/build-all contact-comp (:contacts data))))))

Таким образом, contact-comp просто отображает один контакт. Мы используем om/build-all чтобы сделать все контакты видимыми в поле :contacts в глобальном состоянии. И самая сложная часть — мы используем метод жизненного цикла will-mount для получения контактов с сервера, когда компонент app собирается смонтировать в DOM.

Опять же, в этом коммите должна быть рабочая версия на случай каких-либо проблем.

И если вам понравился Om, я настоятельно рекомендую официальные учебные пособия и серию Zero to Om .