使用 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!” 反而。

Redux 和 Reagent 处理的状态管理概述。

对于本地的、组件级的突变来说,这似乎很简单,但是如果我们有一个具有多个抽象级别的更复杂的应用程序怎么办? 或者,如果我们需要在多个子组件及其子组件之间共享公共状态?

一个更复杂的例子

让我们用一个例子来探讨一下。 在这里,我们将实现一个粗略的登录页面:

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

因此,预期的工作流程是:

  1. 我们等待用户输入他们的用户名和密码并点击提交。
  2. 这将在父组件中触发我们的do-login-io功能。
  3. 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) 状态如何工作。

没有大惊小怪,没有混乱。

使用 Redux/Re-frame 解耦组件意味着干净的状态管理

使用 Redux(通过 re-frame),我们成功地将视图组件从混乱的状态处理中解耦。 扩展我们的状态抽象现在是小菜一碟!

ClojureScript 中的 Redux 真的就是这么简单——你没有理由不尝试一下。

如果您准备好开始破解,我建议您查看精彩的 re-frame 文档和我们的简单示例。 我期待在下面阅读您对这个 ClojureScript 教程的评论。 祝你好运!

相关:使用 Firebase 在 Angular 中进行状态管理