本文概述
欢迎回来阅读《出土的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还通过原子概念为我们提供了一种方便的方式, 让我们在需要时具有可变状态。
ClojureScript中的原子与Java中的AtomicReference非常相似:它提供了一个新对象, 该对象使用并发保证来锁定其内容。就像在Java中一样, 你可以在该对象中放置任何你喜欢的东西-从那时起, 该原子将成为你想要的任何内容的原子引用。
一旦有了原子, 就可以通过使用reset原子设置一个新值!函数(请注意函数中的!–在Clojure语言中, 通常用于表示操作是有状态的或不纯的)。
另请注意, 与Java不同, Clojure并不关心你在原子中添加的内容。它可以是字符串, 列表或对象。动态输入, 宝贝!
(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, 请先阅读这篇文章。)此行为与ClojureScript原子相同, 除了它还会触发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!")}]])
这将显示一个包含<span>的单个<div>, 上面写着”你好, 世界!”和一个按钮, 正如你所期望的那样。按下该按钮将自动使my-atom突变为包含”那里!”。这将触发该组件的重绘, 结果跨度显示为” 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修饰的包装器(我认为), 使它使用起来绝对令人愉悦。
基础
Redux提升了你的应用程序状态, 使组件轻巧。 Reduxified组件只需要考虑:
- 看起来像什么
- 它消耗什么数据
- 它触发什么事件
其余的则在后台处理。
为了强调这一点, 让我们重新在上方重新登录我们的登录页面。
数据库
首先, 我们需要确定应用程序模型的外观。我们通过定义数据的形状来做到这一点, 这些数据可以在整个应用程序中访问。
一个好的经验法则是, 如果数据需要在多个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的规格库来描述数据的外观。这对于像Clojure [Script]这样的动态语言尤其适用。
- 在此示例中, 我们仅跟踪一个全局令牌, 该令牌将在用户登录后代表我们的用户。此令牌是一个简单的字符串。
- 但是, 在用户登录之前, 我们将没有令牌。这由:opt-un关键字表示, 它表示”可选, 不合格”。 (在Clojure中, 常规关键字可能类似于:cat, 而合格关键字可能类似于:animal / cat。合格通常发生在模块级别–这阻止了不同模块中的关键字相互破坏。)
- 最后, 我们指定数据库的默认状态, 即默认状态。
在任何时候, 我们都应该确信数据库中的数据与此处的规范相符。
订阅内容
既然我们已经描述了我们的数据模型, 那么我们需要反映一下我们的视图如何显示该数据。我们已经描述了视图在Redux组件中的样子-现在我们只需要将视图连接到数据库即可。
使用Redux, 我们不会直接访问数据库-这可能会导致生命周期和并发问题。相反, 我们通过订阅将我们的关系注册到数据库的某个方面。
订阅告诉重新框架(和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事件(具有副作用)。
我们将从简单的一开始。重新构架甚至提供了针对此类事件的功能:
(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中的类似。
- 我们不仅返回一个新的数据库, 还返回一个映射, 该映射表示此事件应发生的所有效果(” 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(通过重新框架), 我们成功地将视图组件与状态处理混乱分离了。现在扩展我们的状态抽象只是小菜一碟!
ClojureScript中的Redux确实很容易-你没有理由不尝试一下。
如果你准备好破解, 建议你查看出色的重新整理文档和我们简单的示例。我期待着你阅读下面有关此ClojureScript教程的评论。祝你好运!
相关:使用Firebase在Angular中进行状态管理
评论前必须登录!
注册