Раскопки ClojureScript для фронтенд-разработки
Опубликовано: 2022-03-11Когда вы читаете это введение, в вашей голове, вероятно, возникают две основные мысли:
- Что такое ClojureScript?
- Это не относится к моим интересам.
Но ждать! Вот тут ты ошибаешься, и я тебе это докажу. Если вы готовы потратить 10 минут своего времени, я покажу вам, как ClojureScript может сделать написание интерфейсных и React-приложений увлекательным, быстрым и, что самое главное , функциональным .
Некоторые требования к учебнику по ClojureScript
- Знание Лиспа не требуется. Я сделаю все возможное, чтобы объяснить любые примеры кода, разбросанные в этом сообщении блога!
- Тем не менее, если вы хотите немного предварительно прочитать, я настоятельно рекомендую https://www.braveclojure.com/, универсальный магазин для начала работы с Clojure (и, соответственно, с ClojureScript).
- Clojure и ClojureScript имеют общий язык — я часто буду ссылаться на них одновременно с Clojure[Script] .
- Я предполагаю, что у вас есть знания о React и общие ноу-хау во внешнем интерфейсе.
Как изучить ClojureScript: краткая версия
Так что у вас не так много времени на изучение ClojureScript, и вы просто хотите увидеть, куда все это идет. Прежде всего, что такое ClojureScript?
С веб-сайта ClojureScript: ClojureScript — это компилятор для Clojure, ориентированный на JavaScript. Он генерирует код JavaScript, совместимый с расширенным режимом компиляции оптимизирующего компилятора Google Closure.
Помимо прочего, ClojureScript может многое предложить:
- Это мультипарадигмальный язык программирования с уклоном на функциональное программирование — известно, что функциональное программирование улучшает разборчивость кода , а также помогает писать больше с меньшим количеством кода .
- Он поддерживает неизменяемость по умолчанию — попрощайтесь с целым набором проблем во время выполнения!
- Он ориентирован на данные: код — это данные в ClojureScript. Большинство приложений Clojure[Script] можно свести к набору функций, которые работают с некоторой базовой структурой данных, что делает отладку простой, а код сверхразборчивым.
- Это просто! Начать работу с ClojureScript легко — здесь нет замысловатых ключевых слов и очень мало магии.
- Он имеет фантастическую стандартную библиотеку. В этой штуке есть все.
С этим покончено, давайте откроем эту банку червей на примере:
(defn component [] [:div "Hello, world!"])
Примечание для тех, кто не знаком с диалектами Lisp или ClojureScript: наиболее важными частями этого примера являются :div
, []
и ()
. :div
— это ключевое слово, представляющее элемент <div>
. []
— это вектор, очень похожий на ArrayList
в Java, а ()
— последовательность, очень похожая на LinkedList
. Я коснусь этого более подробно позже в этом посте!
Это самая основная форма компонента React в ClojureScript. Вот и все — просто ключевое слово, строка и целая куча списков.
Тьфу! вы говорите, что это не сильно отличается от «hello world» в JSX или TSX:
function component() { return ( <div> "Hello, world!" </div> ); }
Тем не менее, есть некоторые важные отличия, которые мы можем заметить даже из этого простого примера:
- Нет встроенных языков; все в примере ClojureScript является строкой, ключевым словом или списком.
- Это лаконично ; списки обеспечивают всю необходимую выразительность без избыточности закрывающих тегов HTML.
Эти два небольших различия имеют огромные последствия не только в том, как вы пишете код, но и в том, как вы выражаете себя!
Как это, спросите вы? Давайте окунемся в бой и посмотрим, что еще приготовил для нас ClojureScript…
- Начало работы с языком программирования Elm
- Начало работы с языком программирования Elixir
Строительные блоки
В этом руководстве по ClojureScript я постараюсь не слишком углубляться в то, что делает Clojure[Script] замечательным (а это много вещей, но я отвлекся). Тем не менее, будет полезно охватить некоторые основные концепции, чтобы можно было понять широту того, что мы можем здесь сделать.
Опытные кложуристы и лиспианы могут смело переходить к следующему разделу!
Сначала я должен осветить три основные концепции:
Ключевые слова
В Clojure[Script] есть понятие, называемое ключевым словом. Он находится где-то между константной строкой (скажем, в Java) и ключом. Это символические идентификаторы, которые оцениваются сами по себе .
Например, ключевое слово :cat
всегда будет ссылаться на :cat
и никогда ни на что другое. Как и в Java, вы могли бы сказать:
private static const String MY_KEY = "my_key"; // ... myMap.put(MY_KEY, thing); // ... myMap.get(MY_KEY);
…в Clojure у вас будет просто:
(assoc my-map :my-key thing) (my-map :my-key) ; equivalent to (:my-key my-map) ...nice and flexible!
Также обратите внимание: в Clojure карта является одновременно и коллекцией (ключей к значениям, как в Java HashMap
), и функцией для доступа к ее содержимому. Аккуратный!
Списки
Clojure[Script], будучи диалектом Лиспа, означает, что он уделяет большое внимание спискам. Как я упоминал ранее, есть две основные вещи, о которых нужно знать:
-
[]
— это вектор, очень похожий наArrayList
. -
()
— это последовательность, очень похожая наLinkedList
.
Чтобы создать список вещей в Clojure[Script], вы делаете следующее:
[1 2 3 4] ["hello" "world"] ["my" "list" "contains" 10 "things"] ; you can mix and match types ; in Clojure lists!
Для последовательностей немного по-другому:
'(1 2 3 4) '("hello" "world")
Предварительная '
объясняется в следующем разделе.
Функции
Наконец, у нас есть функции. Функция в Clojure[Script] — это последовательность , которая печатается без предшествующего '
. Первым элементом этого списка является сама функция, а все последующие элементы будут аргументами . Например:
(+ 1 2 3 4) ; -> 10 (str "hello" " " "world") ; -> "hello world" (println "hi!") ; prints "hi!" to the console (run-my-function) ; runs the function named `run-my-function`
Одним из следствий такого поведения является то, что вы можете построить определение функции, не выполняя ее на самом деле! При оценке программы будет выполняться только «голая» последовательность.
(+ 1 1) ; -> 2 '(+ 1 1); -> a list of a function and two numbers
Это станет актуальным позже!
Функции можно определить несколькими способами:
; A normal function definition, assigning the function ; to the symbol `my-function` (defn my-function [arg1 arg2] (+ arg1 arg2)) ; An anonymous function that does the same thing as the above (fn [arg1 arg2] (+ arg1 arg2)) ; Another, more concise variation of the above #(+ %1 %2)
Более пристальное рассмотрение
Итак, теперь, когда мы рассмотрели основы, давайте углубимся в детали, чтобы увидеть, что здесь происходит.
React в ClojureScript обычно выполняется с использованием библиотеки под названием Reagent. Reagent использует Hiccup и его синтаксис для представления HTML. Из вики репозитория Hiccup:
«Иккинг превращает структуры данных Clojure вот так:»
[:a {:href "http://github.com"} "GitHub"]
«В такие строки HTML:»
<a href="http://github.com">GitHub</a>
Проще говоря, первый элемент списка становится типом элемента HTML, а остальные становятся содержимым этого элемента. При желании вы можете предоставить карту атрибутов, которая затем будет прикреплена к этому элементу.
Элементы могут быть вложены друг в друга, просто вложив их в список своего родителя! Это проще всего увидеть на примере:
[:div [:h1 "This is a header"] [:p "And in the next element we have 1 + 1"] [:p (+ 1 1)]]
Обратите внимание, как мы можем поместить любую старую функцию или общий синтаксис Clojure в нашу структуру без явного объявления метода встраивания. В конце концов, это всего лишь список!
И даже лучше, что это оценивает во время выполнения?
[:div [:h1 "This is a header"] [:p "And in the next element we have 1 + 1"] [:p 2]]
Список ключевых слов и содержание, конечно! Нет забавных типов, нет волшебных скрытых методов. Это просто старый добрый список вещей. Вы можете совмещать и играть с этим списком столько, сколько хотите — то, что вы видите, это то, что вы получаете.

С Hiccup, выполняющим макет, и Reagent, выполняющим логику и обработку событий, мы получаем полностью функциональную среду React.
Более сложный пример
Хорошо, давайте еще немного свяжем это с некоторыми компонентами. Одна из волшебных особенностей React (и Reagent) заключается в том, что вы разделяете логику вашего представления и макета на модули, которые затем можно повторно использовать во всем приложении.
Скажем, мы создаем простой компонент, который показывает кнопку и некоторую простую логику:
; widget.cljs (defn component [polite?] [:div [:p (str "Do not press the button" (when polite? ", please."))] [:input {:type "button" :value "PUSH ME" :on-click #(js/alert "What did I tell you?")}]])
Небольшое примечание по именованию: Модули в Clojure обычно имеют пространство имен, поэтому widget.cljs
можно импортировать в пространство имен widget
. Это означает, что функция component
верхнего уровня будет доступна как widget/component
. Мне нравится иметь только один компонент верхнего уровня для каждого модуля, но это предпочтение стиля — вы можете предпочесть назвать функцию вашего компонента как-то вроде polite-component
или widget-component
.
Этот простой компонент представляет нам необязательно вежливый виджет. (when polite? ", please.")
оценивается как ", please."
когда polite? == true
polite? == true
и nil
, если оно false
.
Теперь давайте встроим это в наш app.cljs
:
(defn app [] [:div [:h1 "Welcome to my app"] [widget/component true]])
Здесь мы встраиваем наш виджет в компонент нашего приложения, вызывая его как первый элемент списка — точно так же, как ключевые слова HTML! Затем мы можем передать любые дочерние элементы или параметры компоненту, предоставив их как другие элементы того же списка. Здесь мы просто передаем true
, так что в нашем виджете polite? == true
polite? == true
, и таким образом мы получаем вежливую версию.
Если бы мы сейчас оценили функцию нашего приложения, то получили бы следующее:
[:div [:h1 "Welcome to my app"] [widget/component true]] ; <- widget/component would look more like a ; function reference, but I have kept it ; clean for legibility.
Обратите внимание, что widget/component
не был оценен! (См. раздел «Функции», если вы запутались.)
Компоненты в вашем дереве DOM оцениваются (и, таким образом, преобразуются в настоящие объекты React за кулисами) только в том случае, если они были обновлены, что делает вещи красивыми и быстрыми и снижает сложность, с которой вам приходится иметь дело в любой момент времени.
Более подробную информацию по этому вопросу для интересующихся можно получить в документации Reagent.
Списки до конца
Кроме того, обратите внимание, что DOM — это просто список списков, а компоненты — это просто функции, которые возвращают списки списков. Почему это так важно при изучении ClojureScript?
Потому что все, что вы можете делать с функциями или списками, вы можете делать и с компонентами.
Именно здесь вы начинаете получать комбинированные результаты, используя диалект Лиспа, такой как ClojureScript: ваши компоненты и элементы HTML становятся первоклассными объектами, которыми вы можете манипулировать, как и любыми другими обычными данными! Позвольте мне просто сказать, что еще раз:
Компоненты и HTML-элементы — это первоклассно поддерживаемые объекты в языке Clojure!
Правильно, вы меня слышали. Это похоже на то, что Лиспы были созданы для обработки списков (подсказка: так оно и было).
Это включает в себя такие вещи, как:
- Отображение элементов нумерованного списка:
(def words ["green" "eggs" "and" "ham"]) (defn li-shout [x] [:li (string/uppercase x)) (concat [:ol] (map li-shout words) ; becomes [:ol [:li "GREEN"] [:li "EGGS"] [:li "AND"] [:li "HAM"]]
- Компоненты упаковки:
; in widget.cljs (defn greeting-component [name] [:div [:p (str "Hiya " name "!")]]) ; ... (def shouty-greeting-component #(widget/greeting-component (string/uppercase %))) (defn app [] [:div [:h1 "My App"] [shouty-greeting-component "Luke"]]) ; <- will show Hiya LUKE!
- Внедрение атрибутов:
(def default-btn-attrs {:type "button" :value "I am a button" :class "my-button-class"}) (defn two-button-component [] [:div [:input (assoc default-btn-attrs :on-click #(println "I do one thing"))] [:input (assoc default-btn-attrs :on-click #(println "I do a different thing"))]])
Работа с простыми старыми типами данных, такими как списки и карты, намного проще, чем что-либо, напоминающее класс, и в конечном итоге оказывается намного более мощным!
Образец появляется
Хорошо, подведем итоги. Что показал наш туториал по ClojureScript?
- Все сводится к простейшему функционалу — элементы — это просто списки, а компоненты — это просто функции, возвращающие элементы.
- Поскольку компоненты и элементы являются первоклассными объектами, мы можем писать больше с меньшими затратами .
Эти два пункта идеально вписываются в идеалы Clojure и функционального программирования: код — это данные , которыми нужно манипулировать, а сложность создается за счет соединения менее сложных частей. Мы представляем нашу программу (нашу веб-страницу в этом примере) в виде данных (списков, функций, карт) и сохраняем ее в таком виде до самого последнего момента, когда Reagent берет верх и превращает ее в код React. Это делает наш код пригодным для повторного использования и, что наиболее важно, очень простым для чтения и понимания с очень небольшим количеством магии.
Стать стильным
Теперь мы знаем, как сделать приложение с некоторыми базовыми функциями, так что давайте перейдем к тому, как сделать так, чтобы оно выглядело хорошо. Есть несколько способов приблизиться к этому, самый простой из которых — использовать таблицы стилей и ссылаться на их классы в ваших компонентах:
.my-class { color: red; }
[:div {:class "my-class"} "Hello, world!"]
Это будет работать именно так, как вы ожидаете, представив нам красивое красное «Hello, world!» текст.
Однако зачем заниматься всеми этими проблемами, размещая код представления и логики в ваших компонентах, а затем разделяя ваши стили в таблицу стилей — теперь вам не только нужно искать в двух разных местах, но вы имеете дело с двумя разными языки тоже!
Почему бы не написать наш CSS как код в наших компонентах (см. тему здесь?). Это даст нам кучу преимуществ:
- Все, что определяет компонент, находится в одном месте.
- Уникальность имен классов может быть гарантирована за счет умной генерации.
- CSS может быть динамическим, изменяющимся по мере изменения наших данных.
Лично мне больше всего нравится CSS-in-code с таблицами стилей Clojure (cljss). Встроенный CSS выглядит следующим образом:
;; -- STYLES ------------------------------------------------------------ (defstyles component-style [] {:color "red" :width "100%"}) ;; -- VIEW -------------------------------------------------------------- (defn component [] [:div {:class (component-style)} "Hello, world!"])
defstyles
создает функцию, которая сгенерирует для нас уникальное имя класса (что отлично подходит для всех, кто импортирует наши компоненты).
Есть много других вещей, которые cljss может сделать для вас (составление стилей, анимация, переопределение элементов и т. д.), которые я не буду здесь подробно описывать. Рекомендую убедиться в этом лично!
Сборка частей приложения ClojureScript
Наконец, есть клей, необходимый, чтобы склеить все это вместе. К счастью, помимо файла проекта и index.html
здесь есть как минимум шаблонный код.
Тебе нужно:
- Файл определения вашего проекта,
project.clj
. Основной элемент любого проекта Clojure, он определяет ваши зависимости — даже непосредственно из GitHub — и другие свойства сборки (аналогичноbuild.gradle
илиpackage.json
). -
index.html
, который действует как точка привязки для приложения Reagent. - Некоторый код настройки для среды разработки и, наконец, для запуска вашего приложения Reagent.
Полный пример кода для этого руководства по ClojureScript доступен на GitHub.
Вот так (пока). Надеюсь, я хотя бы немного возбудил ваше любопытство, будь то проверка диалекта Лиспа (Clojure[Script] или что-то еще) или даже попытка создать собственное приложение Reagent! Я обещаю вам, что вы не пожалеете об этом.
Присоединяйтесь ко мне в продолжении этой статьи «Попадание в состояние», где я рассказываю об управлении состоянием с помощью рефрейминга — поздоровайтесь с Redux в ClojureScript!