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
Стоит отметить две вещи, которые уде видны в нашем коде:
- "вёрстка" выполнена с помощью обычных элементов, которыми мы описываем данные — векторов, отображений, ключевых слов, строк
- Обработчики событий — это обычные функции на ClojureScript и больше нет нужды помнить особенности компиляции ClJS в JS
Второй пункт скорее просто приятен. А вот первый демонстрирует одну из особенностей ClojureScript — построение доменноориентированных языков с помощью структур данных или, как ещё говорят, "datadriven 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 [] …)}
.