пятница, 29 января 2016 г.

Reagent - вся модность ReactJS на ClojureScript

Как описывают создатели ReactJS, пользовательский интерфейс становится всё сложнее и сложнее, и тем он сложнее, чем он комплекснее. А интерфейсы усложняются, пытаются в браузере сделать всё. В целом, может, это и неплохо, но потребность в чём-то просто есть.Вообще, про реактивное программирование говорили ещё давно, но вот сейчас оно пошло в широкие массы.

ClojureScript тоже решил не оставаться в стороне и какие-то ребята запили обёртку под это дело. В целом, там же Getting Started и ещё есть куча различных туториалов, где всё подробно расписано. Суть сводится к тому, что есть некоторое состояние приложения (или несколько их), которое инкапсулируется в реактивном атоме (reagent.core/atom), и есть компоненты, которые суть есть кусок DOM, изменяющийся в зависимости от состояния. Собственно, это автоматическое изменение и есть работа ReactJS. Звучит просто? Но, в целом, так оно и есть. Рассмотрим чуточку подробнее на примере игры Memory Game, исходники которого можно глянуть тут.

(def game (r/atom {}))
Который на старте игры инициализируется  следующим:
(-> game'
  (assoc-in [:level] level)
  (assoc-in [:state-changed] true)
  (assoc-in [:w] w)
  (assoc-in [:h] h)
  (assoc-in [:can-open] true)
  (assoc-in [:board] board)
  (assoc-in [:opened] [])
  (assoc-in [:opened-count] 0)
  (assoc-in [:score] 0)
  (assoc-in [:state] :playing))
Собственно, это всё, что хранится в состоянии игры. Ну, а сама инициализация рендеринга:

(r/render-component [game-component] (.getElementById js/document "app"))
Далее, сам компонент game-component является обычной функцией ClojureScript и собирает в себя остальные компоненты:

(defn game-component []
  (let [cur-game @game]
    [:div.container.container-table
     [:div.row
      [:div.col-md-3.col-md-offset-2
       [:div "Restart with level:"]
       [:button.btn.btn-xs.btn-default
        {:type "button"
         :on-click #(start :easy)}
        "Easy"]
       [:button.btn.btn-xs.btn-default
        {:type "button"
         :on-click #(start :normal)}
        "Normal"]
       [:button.btn.btn-xs.btn-default
        {:type "button"
         :on-click #(start :hard)}
        "Hard"]
       [score-component (:score cur-game)]
       [game-state-component (:state cur-game) (:score cur-game)]
       [:p "Rules: " "Open two matched cards and they will stay opened. "
        "Otherwise it will be closed, but you memorize it's locations."]
       [:p "Author: " [:a {:href "https://bitbucket.org/turtle_bazon/"} "Azamat S. Kalimoulline"]]
       [:p [:a {:href "https://bitbucket.org/turtle_bazon/cljs-games/src/"} "sources"]]]
      [board-component]]]))
При этом тут используется расширенный синтаксис квадратных скобочек, который, помимо прочего, означает, что тут необходимо вызвать функцию компонента. При этом не просто вызывать, а как-то что-то сделать и пометить там у себя, чтобы эта функция вызывалась только при изменении той ветки стейта, на основе которой он рендерится. Для этого функция должна принимать аругменты, которые как раз будут веткой (-ами) состояния.

Отдельное внимание стоит уделить компоненту board-component, который рендерит доску NxM размера. По сути, доска состоит из строк, а строки состоят из ячеек. Так вот, получается, что внутри board-component есть повторяющиеся row-component, внутри которых повторяющиеся card-component. Чтобы ReactJS смог разобраться с этим, чтобы не перерендеривать всю строку целиком, ему нужно помочь, задав некоторый идентификатор с помощью "^{:key id}":

(defn row-component [row]
  [:div.board-row
   (for [[id card] row]
     ^{:key id} [card-component id card])])

(defn board-component []
  (let [cur-game @game
        w (:w cur-game)
        h (:h cur-game)
        cards-array (partition
                     w
                     (map (fn [id card]
                            [id card])
                          (for [row (range 0 h)
                                col (range 0 w)]
                            (+ (* w row) col))
                          (:board cur-game)))]
    [:div.board.col-md-5
     (for [[row row-index] (map vector cards-array (range 0 h))]
       ^{:key (str "r" row-index)}
       [row-component row])]))
Ну а вообще, получается вполне симпатично, весь пользовательский интерфейс отрисовывается на основе состояние и изменением состояния контролируется пользовательский интерфейс. Всё чисто и понятно, если нас не подведут Reagent и ReactJS. То есть там происходит какая-то их магия, но пусть.

Итого - очень даже интересная технология, которая позволяет избавиться от жёсткого связывания модели и логики отображения. Из минусов можно отметить, что всё делается в hiccup стиле, что вынуждает верстальщика учить Clojure или, наоборот, программиста учить вёрстку. Что, по моему личному мнению, не очень хорошо. Но как раз для решения таких проблем существует проект kio, который позволяет выделить верстальщика отдельно и радоваться жизни.

Ну а подводя полный итог можно сказать, что уж если мы решили делать интерфейс в вебе, то нам, скорее всего, понадобится использовать что-то подобное, в противном случае он рискует значительно быстрее превратиться в нагромождение непонятной лапши.


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

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