使用 Redux 狀態管理進行頂級控制:ClojureScript 教程
已發表: 2022-03-11歡迎回來觀看 Unearthing ClojureScript 的第二個激動人心的部分! 在這篇文章中,我將介紹認真使用 ClojureScript 的下一個重要步驟:狀態管理——在本例中,使用 React。
使用前端軟件,狀態管理很重要。 開箱即用,有幾種方法可以在 React 中處理狀態:
- 將狀態保持在頂層,並將其(或特定狀態的處理程序)傳遞給子組件。
- 將純度拋到窗外,並使用全局變量或某種 Lovecraftian 形式的依賴注入。
一般來說,這些都不是很好。 將狀態保持在頂層相當簡單,但是將應用程序狀態傳遞給每個需要它的組件會產生大量開銷。
相比之下,擁有全局變量(或其他幼稚的狀態版本)可能會導致難以跟踪的並發問題,從而導致組件無法在您期望的時候更新,反之亦然。
那麼如何解決這個問題呢? 對於那些熟悉 React 的人來說,你可能已經嘗試過 Redux,一個用於 JavaScript 應用程序的狀態容器。 您可能出於自己的意願發現了這一點,大膽地尋找一個可管理的系統來維護狀態。 或者您可能在閱讀 JavaScript 和其他 Web 工具時偶然發現了它。
不管人們最終如何看待 Redux,根據我的經驗,他們通常會產生兩種想法:
- “我覺得我必須使用它,因為每個人都說我必須使用它。”
- “我真的不完全明白為什麼這會更好。”
一般來說,Redux 提供了一個抽象,讓狀態管理適合 React 的反應性。 通過將所有狀態卸載到像 Redux 這樣的系統,您可以保持 React 的純度。 因此,您最終會減少很多頭痛,並且通常會更容易推理。
對於那些剛接觸 Clojure 的人
雖然這可能無法幫助您完全從頭開始學習 ClojureScript,但在這裡我至少會回顧一下 Clojure[Script] 中的一些基本狀態概念。 如果您已經是一位經驗豐富的 Clojurian,請隨意跳過這些部分!
回想一下也適用於 ClojureScript 的 Clojure 基礎知識之一:默認情況下,數據是不可變的。 這對於開發和保證您在時間步 N 創建的內容在時間步 > N 時仍然是相同的非常有用。ClojureScript 還通過atom
概念為我們提供了一種方便的方法來獲得可變狀態,如果我們需要它。
ClojureScript 中的atom
與 Java 中的AtomicReference
非常相似:它提供了一個新對象,該對象通過並發保證鎖定其內容。 就像在 Java 中一樣,你可以在這個對像中放置任何你喜歡的東西——從那時起,這個原子將成為你想要的任何東西的原子引用。
一旦你有了atom
,你可以通過使用reset!
函數(注意函數中的!
——在 Clojure 語言中,這通常用於表示操作是有狀態的或不純的)。
另請注意,與 Java 不同,Clojure 並不關心您在atom
中放入了什麼。 它可以是字符串、列表或對象。 動態打字,寶貝!
(def my-mutable-map (atom {})) ; recall that {} means an empty map in Clojure (println @my-mutable-map) ; You 'dereference' an atom using @ ; -> this prints {} (reset! my-mutable-map {:hello "there"}) ; atomically set the atom (reset! my-mutable-map "hello, there!") ; don't forget Clojure is dynamic :)
Reagent 用它自己的atom
擴展了 atom 的這個概念。 (如果您不熟悉 Reagent,請查看之前的帖子。)這與 ClojureScript atom
的行為相同,除了它還會觸發 Reagent 中的渲染事件,就像 React 的內置狀態存儲一樣。
一個例子:
(ns example (:require [reagent.core :refer [atom]])) ; in this module, atom now refers ; to reagent's atom. (def my-atom (atom "world!")) (defn component [] [:div [:span "Hello, " @my-atom] [:input {:type "button" :value "Press Me!" :on-click #(reset! My-atom "there!")}]])
這將顯示一個<div>
包含一個<span>
說“你好,世界!” 和一個按鈕,正如您所料。 按下該按鈕將自動改變my-atom
以包含"there!"
. 這將觸發組件的重繪,導致 span 顯示“Hello, there!” 反而。
對於本地的、組件級的突變來說,這似乎很簡單,但是如果我們有一個具有多個抽象級別的更複雜的應用程序怎麼辦? 或者,如果我們需要在多個子組件及其子組件之間共享公共狀態?
一個更複雜的例子
讓我們用一個例子來探討一下。 在這裡,我們將實現一個粗略的登錄頁面:
(ns unearthing-clojurescript.login (:require [reagent.core :as reagent :refer [atom]])) ;; -- STATE -- (def username (atom nil)) (def password (atom nil)) ;; -- VIEW -- (defn component [on-login] [:div [:b "Username"] [:input {:type "text" :value @username :on-change #(reset! username (-> % .-target .-value))}] [:b "Password"] [:input {:type "password" :value @password :on-change #(reset! password (-> % .-target .-value))}] [:input {:type "button" :value "Login!" :on-click #(on-login @username @password)}]])
然後,我們將在我們的主app.cljs
中託管這個登錄組件,如下所示:
(ns unearthing-clojurescript.app (:require [unearthing-clojurescript.login :as login])) ;; -- STATE (def token (atom nil)) ;; -- LOGIC -- (defn- do-login-io [username password] (let [t (complicated-io-login-operation username password)] (reset! token t))) ;; -- VIEW -- (defn component [] [:div [login/component do-login-io]])
因此,預期的工作流程是:
- 我們等待用戶輸入他們的用戶名和密碼並點擊提交。
- 這將在父組件中觸發我們的
do-login-io
功能。 -
do-login-io
函數執行一些 I/O 操作(例如登錄服務器並檢索令牌)。
如果這個操作是阻塞的,那麼我們已經有一大堆麻煩了,因為我們的應用程序被凍結了——如果不是,那麼我們就需要擔心異步了!
此外,現在我們需要將此令牌提供給想要對我們的服務器進行查詢的所有子組件。 代碼重構變得更加困難!
最後,我們的組件現在不再是純粹的響應式組件——它現在參與管理應用程序其餘部分的狀態、觸發 I/O 並且通常有點麻煩。
ClojureScript 教程:進入 Redux
Redux 是讓你所有基於狀態的夢想成真的魔杖。 如果實施得當,它提供了一種安全、快速且易於使用的狀態共享抽象。
Redux 的內部工作原理(及其背後的理論)在某種程度上超出了本文的範圍。 相反,我將深入研究一個使用 ClojureScript 的工作示例,希望它能夠以某種方式展示它的能力!
在我們的上下文中,Redux 是由許多可用的 ClojureScript 庫之一實現的; 這一個叫做重新框架。 它提供了一個圍繞 Redux 的 Clojure-ified 包裝器,(在我看來)這使得使用它絕對令人愉悅。
基礎
Redux 提升您的應用程序狀態,讓您的組件輕量級。 一個 Reduxified 組件只需要考慮:
- 它看起來像什麼
- 它消耗什麼數據
- 它觸發什麼事件
其餘的在幕後處理。
為了強調這一點,讓我們 Reduxify 上面的登錄頁面。
數據庫
首先要做的事情是:我們需要決定我們的應用程序模型會是什麼樣子。 我們通過定義數據的形狀來做到這一點,這些數據可以在整個應用程序中訪問。
一個好的經驗法則是,如果數據需要跨多個 Redux 組件使用,或者需要長期存在(就像我們的令牌一樣),那麼它應該存儲在數據庫中。 相反,如果數據是組件本地的(例如我們的用戶名和密碼字段),那麼它應該作為本地組件狀態存在,而不是存儲在數據庫中。

讓我們創建我們的數據庫樣板並指定我們的令牌:
(ns unearthing-clojurescript.state.db (:require [cljs.spec.alpha :as s] [re-frame.core :as re-frame])) (s/def ::token string?) (s/def ::db (s/keys :opt-un [::token])) (def default-db {:token nil})
這裡有幾個有趣的點值得注意:
- 我們使用 Clojure 的
spec
庫來描述我們的數據應該是什麼樣子。 這尤其適用於像 Clojure[Script] 這樣的動態語言。 - 對於這個例子,我們只跟踪一個全局令牌,一旦他們登錄就代表我們的用戶。這個令牌是一個簡單的字符串。
- 但是,在用戶登錄之前,我們不會有令牌。 這由
:opt-un
關鍵字表示,代表“可選的,不合格的”。 (在 Clojure 中,常規關鍵字類似於:cat
,而限定關鍵字可能類似於:animal/cat
。限定通常發生在模塊級別——這可以防止不同模塊中的關鍵字相互衝突。) - 最後,我們指定數據庫的默認狀態,即它的初始化方式。
在任何時候,我們都應該確信我們數據庫中的數據符合我們的規範。
訂閱
現在我們已經描述了我們的數據模型,我們需要反映我們的視圖如何顯示該數據。 我們已經描述了我們的視圖在 Redux 組件中的樣子——現在我們只需要將我們的視圖連接到我們的數據庫。
使用 Redux,我們不會直接訪問我們的數據庫——這可能會導致生命週期和並發問題。 相反,我們通過訂閱註冊我們與數據庫方面的關係。
訂閱告訴 re-frame(和 Reagent)我們依賴於數據庫的一部分,如果該部分被改變,那麼我們的 Redux 組件應該被重新渲染。
訂閱的定義非常簡單:
(ns unearthing-clojurescript.state.subs (:require [re-frame.core :refer [reg-sub]])) (reg-sub :token ; <- the name of the subscription (fn [{:keys [token] :as db} _] ; first argument is the database, second argument is any token)) ; args passed to the subscribe function (not used here)
在這裡,我們註冊了一個訂閱——對令牌本身。 訂閱只是訂閱的名稱,以及從數據庫中提取該項目的函數。 我們可以對這個值做任何我們想做的事情,並在這裡盡可能多地改變視圖; 但是,在這種情況下,我們只是從數據庫中提取令牌並返回它。
您可以使用訂閱做很多很多事情——例如在數據庫的子部分上定義視圖以更緊密地重新渲染——但我們現在會保持簡單!
活動
我們有我們的數據庫,我們有我們的數據庫視圖。 現在我們需要觸發一些事件! 在這個例子中,我們有兩種事件:
- 將新令牌寫入數據庫的純事件(沒有副作用)。
- 通過一些客戶端交互出去並請求我們的令牌的 I/O 事件(具有副作用)。
我們將從簡單的開始。 Re-frame 甚至為這種事件提供了一個功能:
(ns unearthing-clojurescript.state.events (:require [re-frame.core :refer [reg-event-db reg-event-fx reg-fx] :as rf] [unearthing-clojurescript.state.db :refer [default-db]])) ; our start up event that initialises the database. ; we'll trigger this in our core.cljs (reg-event-db :initialise-db (fn [_ _] default-db)) ; a simple event that places a token in the database (reg-event-db :store-login (fn [db [_ token]] (assoc db :token token)))
同樣,這裡非常簡單——我們定義了兩個事件。 第一個是初始化我們的數據庫。 (看看它是如何忽略它的兩個參數的?我們總是用我們的default-db
初始化數據庫!)第二個是一旦我們得到它就存儲我們的令牌。
請注意,這些事件都沒有副作用——沒有外部調用,根本沒有 I/O! 這對於維護神聖的 Redux 進程的神聖性非常重要。 不要讓它變得不純,以免你希望 Redux 的憤怒降臨到你身上。
最後,我們需要登錄事件。 我們將它放在其他下面:
(reg-event-fx :login (fn [{:keys [db]} [_ credentials]] {:request-token credentials})) (reg-fx :request-token (fn [{:keys [username password]}] (let [token (complicated-io-login-operation username password)] (rf/dispatch [:store-login token]))))
reg-event-fx
函數與reg-event-db
非常相似,儘管有一些細微的差別。
- 第一個論點不再只是數據庫本身。 它包含許多其他可用於管理應用程序狀態的東西。
- 第二個參數很像
reg-event-db
。 - 我們不只是返回新的
db
,而是返回一個表示該事件應該發生的所有效果(“fx”)的映射。 在這種情況下,我們簡單地調用:request-token
效果,定義如下。 其他有效效果之一是:dispatch
,它只是調用另一個事件。
一旦我們的效果被調度,我們的:request-token
效果就會被調用,它會執行我們長時間運行的 I/O 登錄操作。 一旦完成,它會愉快地將結果分派回事件循環,從而完成循環!
ClojureScript 教程:最終結果
所以! 我們已經定義了我們的存儲抽象。 組件現在是什麼樣子的?
(ns unearthing-clojurescript.login (:require [reagent.core :as reagent :refer [atom]] [re-frame.core :as rf])) ;; -- STATE -- (def username (atom nil)) (def password (atom nil)) ;; -- VIEW -- (defn component [] [:div [:b "Username"] [:input {:type "text" :value @username :on-change #(reset! username (-> % .-target .-value))}] [:b "Password"] [:input {:type "password" :value @password :on-change #(reset! password (-> % .-target .-value))}] [:input {:type "button" :value "Login!" :on-click #(rf/dispatch [:login {:username @username :password @password]})}]])
還有我們的應用組件:
(ns unearthing-clojurescript.app (:require [unearthing-clojurescript.login :as login])) ;; -- VIEW -- (defn component [] [:div [login/component]])
最後,在某個遠程組件中訪問我們的令牌非常簡單:
(let [token @(rf/subscribe [:token])] ; ... )
把它們放在一起:
沒有大驚小怪,沒有混亂。
使用 Redux/Re-frame 解耦組件意味著乾淨的狀態管理
使用 Redux(通過 re-frame),我們成功地將視圖組件從混亂的狀態處理中解耦。 擴展我們的狀態抽象現在是小菜一碟!
ClojureScript 中的 Redux 真的就是這麼簡單——你沒有理由不嘗試一下。
如果您準備好開始破解,我建議您查看精彩的 re-frame 文檔和我們的簡單示例。 我期待在下面閱讀您對這個 ClojureScript 教程的評論。 祝你好運!