Rum

В мире JavaScript популярным средством построения графических интерфейсов является библиотека React. И так уж вышло, что библиотека эта резонирует с идеями функционального программирования: чистые функции без побочных эффектов вроде прямого изменения структуры документа, передача текущего состояния через аргументы функций вместо использования локальных изменяемых состояний.

Да, современный React не запрещает использование компонент с собственными кусочками состояния, да и от чистых функций часто уходят в сторону классов. Но именно в мире ФП принято React использовать по старинке, в виде функций из состояния в представление.

React можно использовать в ClJS и напрямую через JavaScript interoperability, но гораздо удобнее взять готовую обёртку. Rum — одна из таких обёрток. И это довольно тонкая обёртка в сравнении с другими библиотеками и фреймворками: вы имеете полную свободу во всём, кроме "отрисовки" интерфейса — Rum берёт на себя только эту часть работы.

Установка Rum

Чтобы добавить Rum в проект, вам нужно в shadow-cljs.edn упомянуть библиотеку вместе с версией:

 …
 :dependencies
 [[rum "0.12.10"]]
 …

Теперь останется только перезапустить сервер Shadow ClJS. Но перед новым запуском сервера стоит установить ещё и соответствующие библиотеки для JavaScript. Это делается привычно силами NPM:

$ npm install react@16 react-dom@16

Версия 16 была выбрана потому, что Rum не использует новые штуки, появившиеся в React в последнее время. Но использует кое-какие старые, поэтому чтобы не видеть лишних предупреждений в консоли браузера, стоит остановиться на "стабильной" версии react@16. Впрочем, вы можете попробовать установить и самую свежую в качестве эксперимента.

Теперь, когда зависимости установлены, запустите сервер Shadow ClJS.

Первое использование

Перепишем счётчик с помощью Rum. Для этого импортируем модуль rum.core (изменения коснутся файла app.cljs):

(ns com.frontend.app
  (:require [rum.core :as rum]))

;; ^ здесь :as означает "импортировать под именем таким-то"

Теперь нужно удалить объявления функций inc-counter и dec-counter — сможем обойтись без них. А функцию draw нужно изменить таким образом:

(rum/defc draw < rum/reactive []
  [:h1
   (str (rum/react counter))
   [:button {:on-click (fn [] (swap! counter inc))} "+"]
   [:button {:on-click (fn [] (swap! counter dec))} "-"]])

Здесь defc выступает как аналог defn, но объявляет не обычную функцию, а "компонент". Последовательность "< rum/reactive" называется добавлением "примеси" ("mixin"). Примеси добавляют функции­компоненту особые свойства.

Примесь rum/reactive делает компонент реактивным — реагирующим на изменения некоторых ссылок. В данном случае интерфейс будет реагировать на изменение атома counter, потому что в "теле" компонента этот атом использован в вызове (rum/react counter). Такой вызов работает как разыменовывание ссылки и одновременно с этим подсказывает компоненту, за какими ссылками следует следить. При этом вам не нужно самостоятельно оформлять никакие подписки — если, конечно, вы не наблюдаете за атомами в коде, не связанном с построением GUI.

Компонент определён, теперь его нужно "смонтировать" — привязать к уже имеющемуся узлу дереве документа. Делается это так:

(defn init []
  (rum/mount (draw) (.querySelector js/document "#root")))

Теперь счётчик должен снова заработать. А ведь кода стало меньше раза в три!

Rum и HTML

Стоит отметить две вещи, которые уде видны в нашем коде:

  1. "вёрстка" выполнена с помощью обычных элементов, которыми мы описываем данные — векторов, отображений, ключевых слов, строк
  2. Обработчики событий — это обычные функции на ClojureScript и больше нет нужды помнить особенности компиляции ClJS в JS

Второй пункт скорее просто приятен. А вот первый демонстрирует одну из особенностей ClojureScript — построение доменно­ориентированных языков с помощью структур данных или, как ещё говорят, "data­driven design". Строить языки под задачу вообще любят пользователи большинства лиспоподобных языков, но именно в Clojure предпочитают явно отделять что-то, описанное с помощью векторов и отображений, от "обычного кода", записанного с помощью списков. Считается, что это разделение идёт на пользу читаемости.

Итак, HTML в Rum представлен как дерево из вложенных друг в друга векторов. Первый элемент вектора описывает имя тега, CSS­классы, ему назначенные и идентификатор в любых разумных сочетаниях:

  • :div#content — это <div id="content">
  • :.counter — это <div class="counter">, потому что тег div подразумевается
  • :span#foo.bar.baz — это <span id="foo" class="bar baz">

Если нужно добавить какие-то дополнительные атрибуты тегу, то вторым элементов вектора указывается отображение, где имена атрибутов кодируются ключевыми словами, а значения строками (или тем, что безопасно приводится к строке). К примеру, обработчики нажатия у тегов button в нашем коде записаны как {:on-click (fn [] …)}.