为前端开发挖掘 ClojureScript

已发表: 2022-03-11

阅读此介绍时,您的脑海中可能有两个主要想法:

  1. 什么是 ClojureScript?
  2. 这与我的兴趣无关。

可是等等! 这就是你错的地方——我会向你证明这一点。 如果你愿意花 10 分钟的时间,我将向你展示 ClojureScript 如何让编写前端和 React-y 应用程序变得有趣、快速并且——最重要的是——实用

一些 ClojureScript 教程先决条件

  • 不需要 Lisp 知识。 我会尽力解释散布在这篇博文中的任何代码示例!
  • 但是,如果您确实想进行一些预读,我强烈推荐 https://www.braveclojure.com/,它是 Clojure 入门的一站式商店(以及扩展的 ClojureScript)。
  • Clojure 和 ClojureScript 共享一种通用语言——我经常将它们同时称为Clojure[Script]
  • 确实假设你有 React 的知识和一般的前端知识。

如何学习 ClojureScript:简短版

所以你没有太多时间来学习 ClojureScript,你只想看看整个事情的进展。 首先,什么是 ClojureScript?

来自 ClojureScript 网站: ClojureScript 是针对 JavaScript 的 Clojure 编译器。 它发出与 Google Closure 优化编译器的高级编译模式兼容的 JavaScript 代码。

除其他外,ClojureScript 提供了很多东西:

  • 它是一种具有函数式编程精益的多范式编程语言——众所周知,函数式编程可以提高代码的易读性并帮助您用更少的代码编写更多的代码
  • 它默认支持不变性——告别一整套运行时问题!
  • 它是面向数据的:代码是ClojureScript 中的数据。 大多数 Clojure[Script] 应用程序都可以简化为一组对一些底层数据结构进行操作的函数,这使得调试变得简单并且代码超级易读。
  • 这很简单! ClojureScript 入门很容易——没有花哨的关键字,也没有什么魔力。
  • 它有一个很棒的标准库。 这东西应有尽有。

有了这个,让我们用一个例子来打开这个蠕虫罐:

 (defn component [] [:div "Hello, world!"])

不熟悉 Lisp 方言或 ClojureScript 的人请注意:此示例中最重要的部分是:div[]() :div是代表<div>元素的关键字。 []是一个向量,很像 Java 中的ArrayList ,而()是一个序列,很像LinkedList 我将在这篇文章的后面更详细地讨论这个问题!

这是 ClojureScript 中 React 组件的最基本形式。 就是这样——只是一个关键字、一个字符串和一大堆列表。

呸! 你说,这与 JSX 或 TSX 中的“hello world”没有太大区别:

 function component() { return ( <div> "Hello, world!" </div> ); }

但是,即使从这个基本示例中,我们也可以发现一些关键的差异:

  • 没有嵌入式语言; ClojureScript 示例中的所有内容都是字符串、关键字或列表。
  • 简洁; 列表提供了我们需要的所有表现力,而没有 HTML 结束标记的冗余。

这两个小的差异会产生巨大的影响,不仅影响您编写代码的方式,还影响您表达自己的方式!

怎么样,你问? 让我们加入战斗,看看 ClojureScript 还为我们准备了什么……

有关的:
  • Elm 编程语言入门
  • Elixir 编程语言入门

建筑模块

在整个 ClojureScript 教程中,我将尽量不深入挖掘是什么让 Clojure[Script] 变得如此出色(这是很多东西,但我离题了)。 尽管如此,涵盖一些基本概念还是很有用的,因此可以掌握我们在这里可以做的事情的广度。

对于那些有经验的 Clojuristas 和 Lispians,请随意跳到下一部分!

我需要首先介绍三个主要概念:

关键词

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] 是一种 Lisp 方言,意味着它非常重视列表。 正如我之前提到的,有两个主要的事情需要注意:

  1. []是一个向量,很像ArrayList
  2. ()是一个序列,很像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] 中的函数是一个在没有前置'情况下输入的序列。 该列表的第一个元素是函数本身,所有以下元素都是arguments 。 例如:

 (+ 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)

仔细检查

现在我们已经介绍了基础知识,让我们深入了解一下这里发生了什么。

ClojureScript 中的 React 通常使用一个名为 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-componentwidget-component类的名称。

这个简单的组件为我们提供了一个可选的礼貌小部件。 (when polite? ", please.")评估为", please." 什么时候polite? == true polite? == truenil当它是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 对象),这使事情保持美观和快速,并减少您在任何时间点必须处理的复杂性。

感兴趣的人可以在试剂文档中获得有关此主题的更多详细信息。

一路向下列表

此外,请注意 DOM 只是一个列表列表,而组件只是返回列表列表的函数。 为什么这在您学习 ClojureScript 时如此重要?

因为你可以对函数或列表做任何事情,你也可以对组件做任何事情。

这就是你开始使用像 ClojureScript 这样的 Lisp 方言获得复合回报的地方:你的组件和 HTML 元素成为你可以像任何其他普通数据一样操作的一流对象! 让我再说一遍:

组件和 HTML 元素是 Clojure 语言中一流的支持对象!

没错,你听到了。 这几乎就像 Lisps 被设计用于处理列表(提示:它们是。)

这包括以下内容:

  • 映射编号列表的元素:
 (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!"]

这将完全按照您的预期进行,向我们展示一个美丽的红色“你好,世界!” 文本。

但是,为什么要费尽心机将视图和逻辑代码并置到组件中,然后将样式分离到样式表中——您现在不仅需要查看两个不同的位置,而且还要处理两个不同的语言呢!

为什么不将我们的 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.gradlepackage.json 。)
  • 充当 Reagent 应用程序绑定点的index.html
  • 一些用于开发环境的设置代码,最后用于启动您的 Reagent 应用程序。

您可以在 GitHub 上找到此 ClojureScript 教程的完整代码示例。

就是这样(现在)。 希望我至少激起了您的好奇心,无论是检查 Lisp 方言(Clojure[Script] 或其他),还是尝试制作自己的 Reagent 应用程序! 我保证你不会后悔的。

加入我的后续文章,进入一个状态,我在这里讨论使用 re-frame 的状态管理——向 ClojureScript 中的 Redux 打个招呼!