作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
卢克·汤姆林的头像

Luke Tomlin

Luke拥有计算机科学和数学硕士学位,擅长函数式编程. 在谷歌实习开启了他强大的开发生涯.

Share

欢迎回来参加第二部分 Unearthing ClojureScript! In this post, 我将介绍认真使用ClojureScript的下一个重要步骤:状态管理——在本例中, using React.

对于前端软件,状态管理是一件大事. 开箱即用,有几种方法可以处理状态in React:

  • 将状态保持在最高级别, 并将其(或特定状态的处理程序)传递给子组件.
  • 将纯度抛到窗外,使用全局变量或某种lovecraft形式的依赖注入.

一般来说,这两者都不好. 将状态保存在顶层是相当简单的, 但是,将应用程序状态传递给每个需要它的组件需要大量的开销.

By comparison, 使用全局变量(或状态的其他原始版本)可能导致难以跟踪的并发性问题, 导致组件在您期望更新时没有更新, or vice versa.

那么如何解决这个问题呢? 对于熟悉React的人来说,您可能已经尝试过Redux,一个状态容器 JavaScript apps. 你可能是自己发现的, 大胆探索可管理的国家维护体系. 或者您可能只是在阅读JavaScript和其他web工具时偶然发现了它.

不管人们如何看待Redux, 根据我的经验,他们通常会有两种想法:

  • “我觉得我必须使用它,因为每个人都说我必须使用它.”
  • “我真的不完全明白为什么这样更好.”

一般来说,Redux提供了一个抽象,使状态管理适合于 reactive nature of React. 通过将所有的有状态性卸载到Redux这样的系统中,您可以保留 purity of React. 这样你就不会那么头疼了,而且通常也更容易推理了.

For Those New to Clojure

虽然这可能无法帮助您完全从头开始学习ClojureScript, 在这里,我将至少回顾一下Clojure[Script]中的一些基本状态概念。. 如果你已经 a seasoned Clojurian!

回想一下Clojure的一个基础,它也适用于ClojureScript:默认情况下, data is immutable. This is great for developing and having guarantees that what you create at timestep N is still the same at timestep > N. ClojureScript还为我们提供了一种方便的方式来拥有可变状态,如果我们需要的话 atom concept.

An atom 在ClojureScript中非常类似于 AtomicReference 在Java中:它提供了一个新对象,通过并发性保证锁定其内容. Just like in Java, 从那时起,你可以把任何你喜欢的东西放在这个物体上, 这个原子将是你想要的任何东西的原子引用.

Once you have your atom,可以在其中自动设置一个新值 reset! function (note the ! 在函数中(在Clojure语言中,这通常用于表示操作是有状态的或不纯的).

还要注意,与java不同,clojure并不关心您在 atom. 它可以是字符串、列表或对象. Dynamic typing, baby!

(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! 我的可变映射"你好!")  ; don't forget Clojure is dynamic :)

试剂用它自己扩展了原子的概念 atom. (如果您不熟悉Reagent,请查看 the post before this.)这与ClojureScript的行为相同 atom, 除了它还会触发Reagent中的渲染事件,就像React的内置状态存储一样.

An example:

(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!")}]])

This will show a single

containing a saying “Hello, world!和一个纽扣,正如你所料. 按下那个按钮就会自动变异 my-atom to contain "there!". 这将触发重新绘制组件,导致span显示“Hello, there”!” instead.

概述由Redux和Reagent处理的状态管理.

这对当地人来说似乎很简单, component-level mutation, 但是,如果我们有一个更复杂的应用程序,它有多个抽象层次? 或者如果我们需要在多个子组件和它们的子组件之间共享公共状态?

一个更复杂的例子

让我们通过一个例子来探讨这个问题. 这里我们将实现一个粗略的登录页面:

(ns unearthing-clojurescript.login
  (:require [reagent.核心:作为试剂:参考[原子]])

;; -- 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, like so:

(ns unearthing-clojurescript.app
  (:要求[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
    [登录/组件do-login-io]])

预期的工作流程如下:

  1. 我们等待用户输入他们的用户名和密码,然后点击提交.
  2. This will trigger our do-login-io 函数在父组件中.
  3. The do-login-io 函数执行一些I/O操作(例如在服务器上登录并检索令牌).

如果此操作阻塞, 那我们就麻烦大了, 因为我们的应用程序被冻结了——如果不是的话, 然后我们要考虑异步!

Additionally, 现在,我们需要将这个令牌提供给希望对服务器执行查询的所有子组件. 代码重构变得更加困难了!

最后,我们的组件现在不再是纯粹的 reactive它现在是管理应用程序其余部分状态的同谋, 触发I/O,通常有点讨厌.

ClojureScript教程:输入Redux

Redux是一根魔杖,可以让你所有的国家梦想成真. 如果实现得当,它提供了一种安全、快速且易于使用的状态共享抽象.

Redux的内部工作原理(及其背后的理论)在某种程度上超出了本文的范围. Instead, 我将深入研究一个使用ClojureScript的工作示例, 这应该能在某种程度上证明它的能力!

In our context, Redux is implemented by one of the many ClojureScript libraries available; this one called re-frame. 它为Redux提供了一个clojure化的包装器,(在我看来)使用起来绝对令人愉悦.

The Basics

Redux提升应用程序状态,使组件保持轻量级. Reduxified组件只需要考虑:

  • What it looks like
  • What data it consumes
  • What events it triggers

其余的都在幕后处理.

为了强调这一点,让我们重新定义上面的登录页面.

The Database

首先要做的是:我们需要决定我们的应用程序模型将是什么样子. 我们通过定义 shape 我们的数据,这些数据将在整个应用程序中被访问.

一个好的经验法则是,如果数据需要跨多个Redux组件使用, 或者需要长期存在(就像我们的令牌一样), 然后应该存储在数据库中. By contrast, 如果数据是组件的本地数据(比如我们的用户名和密码字段),那么它应该作为本地组件状态存在,而不是存储在数据库中.

让我们创建我们的数据库样板并指定我们的令牌:

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

这里有几个有趣的地方值得注意:

  • We use Clojure’s spec library to describe 我们的数据应该是什么样子. 这在像Clojure[Script]这样的动态语言中尤其适用。.
  • For this example, 我们只跟踪一个全局令牌,它将代表我们的用户一旦登录. 这个令牌是一个简单的字符串.
  • 但是,在用户登录之前,我们没有令牌. 这是用 :opt-un 关键字,代表“可选的,非限定的”.(在Clojure中,常规关键字是这样的 :cat,而限定关键字可能是这样的 :animal/cat. 限定通常在模块级别进行,这可以防止不同模块中的关键字相互攻击.)
  • 最后,我们指定数据库的默认状态,这就是数据库初始化的方式.

在任何时候,我们都应该确信数据库中的数据与这里的规范匹配.

Subscriptions

现在我们已经描述了我们的数据模型,我们需要反映我们的 view shows that data. 我们已经描述了Redux组件中的视图,现在我们只需要将视图连接到数据库.

With Redux, 我们不直接访问数据库——这可能导致生命周期和并发性问题. 相反,我们将关系注册到数据库的一个方面 subscriptions.

订阅告诉reframe(和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)

在这里,我们注册一个订阅——对令牌本身. 订阅只是订阅的名称, 以及从数据库中提取该项的函数. 我们可以对这个值做任何我们想做的, and mutate the view as much as we like here; however, in this case, 我们只是从数据库中提取令牌并返回它.

There is much, much 您可以使用订阅做更多的事情—例如在数据库的子部分上定义视图,以便在重新呈现时更严格地限定范围—但是我们现在将保持简单!

Events

我们有数据库,我们有数据库的视图. 现在我们需要触发一些事件! 在这个例子中,我们有两种事件:

  • The pure event (having no 将新令牌写入数据库的副作用.
  • The I/O event (having 通过一些客户端交互出去请求我们的令牌的副作用.

我们从简单的开始. reframe甚至为这类事件提供了一个函数:

(ns unearthing-clojurescript.state.events
  (:require [re-frame.Core:参考[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进程的神圣性是非常重要的. 不要让它变得不纯洁以免你希望雷杜克斯的愤怒降临到你身上.

最后,我们需要登录事件. 我们把它放在其他的下面:

(reg-event-fx
  :login
  (fn [{:keys [db]} [_ credentials]]
    {:请求令牌凭证}))

(reg-fx
  :request-token
  (fn [{:keys[用户名密码]}]
    (let [token (complicated-io-login-operation username - password)]
      (rf/dispatch [:store-login token]))))

The reg-event-fx 功能很大程度上类似于 reg-event-db,尽管有一些细微的差别.

  • 第一个参数不再仅仅是数据库本身. 它包含了许多其他可以用来管理应用程序状态的东西.
  • 第二个参数很像 reg-event-db.
  • 而不仅仅是归还新的 db, 相反,我们返回一个表示该事件应该发生的所有效果(“fx”)的映射. 在本例中,我们简单地调用 :request-token 效果,定义如下. 另一个有效的效果是 :dispatch,它只是调用另一个事件.

一旦我们的效果消散,我们的 :request-token effect,它执行长时间运行的i /O登录操作. Once this is finished, 它愉快地将结果分派回事件循环, thus completing the cycle!

ClojureScript教程:最终结果

So! 我们已经定义了存储抽象. 组件现在是什么样子?

(ns unearthing-clojurescript.login
  (:require [reagent.核心:作为试剂:参考[原子]]
            [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]}}]])

And our app component:

(ns unearthing-clojurescript.app
  (:要求[unearthing-clojurescript.login :as login]))
   
;; -- VIEW --

(defn component
  []
  [:div
    [login/component]])

最后,在某个远程组件中访问我们的令牌就像这样简单:

(let [token @(rf/subscribe [:token])]
  ; ...
  )

Putting it all together:

在登录示例中,本地状态和全局(Redux)状态如何工作.

No fuss, no muss.

用Redux/ reframe解耦组件意味着干净的状态管理

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

在ClojureScript中的Redux is 这么简单——你没有理由不去尝试一下.

如果你已经准备好了,我建议你去看看 神奇的重新框架文件 and our simple worked example. 我期待着阅读您对下面的ClojureScript教程的评论. Best of luck!

Understanding the basics

  • What is a Redux state?

    Redux状态指的是Redux用来管理应用程序状态的单个存储. 这个存储完全由Redux控制,不能从应用程序本身直接访问.

  • Is Redux event sourcing?

    不,Redux是一种独立于事件源模式的技术. Redux的灵感来自另一种叫做Flux的技术.

  • What is a Redux container?

    Redux容器(或者简称为“容器”)是订阅Redux状态的React组件, 当该部分状态发生变化时接收更新.

  • Is Redux a framework?

    是的,Redux在web应用程序中提供了一个围绕状态管理的框架.

  • What is ClojureScript?

    ClojureScript是一个针对JavaScript的Clojure编译器. 它通常用于使用Clojure语言构建web应用程序和库.

聘请Toptal这方面的专家.
Hire Now

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

Toptal Developers

Join the Toptal® community.