為前端開發挖掘 ClojureScript
已發表: 2022-03-11閱讀此介紹時,您的腦海中可能有兩個主要想法:
- 什麼是 ClojureScript?
- 這與我的興趣無關。
可是等等! 這就是你錯的地方——我會向你證明這一點。 如果你願意花 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 方言,意味著它非常重視列表。 正如我之前提到的,有兩個主要的事情需要注意:
-
[]
是一個向量,很像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] 中的函數是一個在沒有前置'
情況下輸入的序列。 該列表的第一個元素是函數本身,所有以下元素都是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-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 對象),這使事情保持美觀和快速,並減少您在任何時間點必須處理的複雜性。
感興趣的人可以在試劑文檔中獲得有關此主題的更多詳細信息。
一路向下列表
此外,請注意 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.gradle
或package.json
。) - 充當 Reagent 應用程序綁定點的
index.html
。 - 一些用於開發環境的設置代碼,最後用於啟動您的 Reagent 應用程序。
您可以在 GitHub 上找到此 ClojureScript 教程的完整代碼示例。
就是這樣(現在)。 希望我至少激起了您的好奇心,無論是檢查 Lisp 方言(Clojure[Script] 或其他),還是嘗試製作自己的 Reagent 應用程序! 我保證你不會後悔的。
加入我的後續文章,進入一個狀態,我在這裡討論使用 re-frame 的狀態管理——向 ClojureScript 中的 Redux 打個招呼!