authors are vetted experts in their fields 和 write on topics in which they have demonstrated experience. All of our content is peer re视图ed 和 validated by Toptal experts in the same field.
卢克·汤姆林

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

分享
< div >

欢迎回来参加第二部分 挖掘ClojureScript的! 在这篇文章中, I’m going to cover the next big step for getting serious with ClojureScript: 状态 management—在这种情况下, 使用的反应.

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

  • 将状态保持在最高级别, 和 passing it (or h和lers for a particular piece of 状态) down to child components.
  • Throwing 纯度 out of the window 和 有 global variables or some Lovecraftian form of dependency injection.

一般来说,这两者都不好. 将状态保存在顶层是相当简单的, but then there’s a large amount of overhead to passing down 应用程序lication 状态 to every component that needs it.

相比之下, 有 global variables (or other naive versions of 状态) can result in hard-to-trace concurrency issues, 导致组件在您期望更新时没有更新, 反之亦然.

那么如何解决这个问题呢? For those of you who are familiar with 反应, you may have tried out 回来的, a 状态 container for JavaScript 应用程序. 你可能是自己发现的, 大胆探索可管理的国家维护体系. Or you might have just stumbled across it while reading about JavaScript 和 other web tooling.

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

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

Generally speaking, 回来的 provides an abstraction that lets 状态 management fit within the 无功 反应的本质. 通过将所有的有状态性卸载到回来的这样的系统中,您可以保留 纯度 的反应. Thus you’ll end up with a lot less headaches 和 generally something that’s a lot easier to reason about.

对于那些刚接触Clojure的人

虽然这可能无法帮助您完全从头开始学习ClojureScript, 在这里,我将至少回顾一下Clojure[Script]中的一些基本状态概念。. 如果你已经 一个经验丰富的克洛朱利安人!

回想一下Clojure的一个基础,它也适用于ClojureScript:默认情况下, 数据是不可变的. This is great for developing 和 有 guarantees that what you create at timestep N is still the same at timestep > N. ClojureScript also provides us with a convenient way to have mutable 状态 if we need it, via the 原子 概念.

An 原子 在ClojureScript中非常类似于 AtomicReference in Java: It provides a new object that locks its contents with concurrency guarantees. 和Java一样, 从那时起,你可以把任何你喜欢的东西放在这个物体上, 这个原子将是你想要的任何东西的原子引用.

一旦你有了 原子,可以在其中自动设置一个新值 重置! 函数(请注意 ! in the function—in the Clojure language this is often used to signify that an operation is 状态ful or impure).

还要注意,与java不同,clojure并不关心您在 原子. 它可以是字符串、列表或对象. 动态打字,宝贝!

(def my-mutable-map (原子 {})) ; recall that {} means an empty map in Clojure

(println @my-mutable-map) ; You 'dereference' an 原子 using @
                          ; -> this prints {}

(重置! my-mutable-map {:hello “有"}) ; 原子ically set the 原子
(重置! 我的可变映射"你好!")  ; don't forget Clojure is dynamic :)

试剂用它自己扩展了原子的概念 原子. (如果您不熟悉Reagent,请查看 之前的帖子.)这与ClojureScript的行为相同 原子, except it also triggers render Events in Reagent, just like 反应’s in-built 状态 store.

一个例子:

(ns的例子
  (:要求[试剂.core :refer [原子]])) ; in this module, 原子 now refers
                                           ; to reagent's 原子.

原子(原子)世界!"))

(defn组件
  []
  [: div
    [:span "Hello, " @my-原子]
    [:input {:type "button"
             :value“按我”!"
             :点击#(重置! My-原子”!")}]])

这将显示一个单

包含一个 说:“你好,世界!和一个纽扣,正如你所料. 按下那个按钮就会自动变异 my-原子 包含 “有!". That will trigger a redraw of the component, resulting in the span saying “Hello, there!”而不是.

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

这对当地人来说似乎很简单, 级变异, but what if we have a more complicated 应用程序lication that has multiple levels of abstraction? Or if we need to share common 状态 between multiple sub-components, 和 their sub-components?

一个更复杂的例子

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

(ns unearthing-clojurescript.登录
  (:要求[试剂.核心:作为试剂:参考[原子]])

;; -- STATE --

(def username (原子 nil))
(def password (原子 nil))

;; -- VIEW --

(defn组件
  (在登录)
  [: div
   [b“用户名”):
   [:input {:type "text"
            @用户名:价值
            :变化#(重置! username (-> % .—target .值)})
   [b“密码”):
   [:input {:type "password"
            :价值@password
            :变化#(重置! password (-> % .—target .值)})
   [:input {:type "button"
            :价值”登录!"
            :on-click #(on-登录 @username @password)}]]

然后,我们将在我们的主组件中托管这个登录组件 应用程序.cljs,像这样:

(ns unearthing-clojurescript.应用程序
  (:要求[unearthing-clojurescript.登录:作为登录]))

;; -- STATE

(def token (原子 nil))

;; -- LOGIC --

(defn do-登录-io
  (用户名密码)
  (let [t (complicated-io-登录-operation username - password)]
    (重置! 令牌t)))
    
;; -- VIEW --

(defn组件
  []
  [: div
    [登录/组件do-登录-io]])

预期的工作流程如下:

  1. 我们等待用户输入他们的用户名和密码,然后点击提交.
  2. 这将触发我们 do-登录-io 函数在父组件中.
  3. do-登录-io function does some I/O operation (such as logging in on a server 和 retrieving a token).

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

另外, now we need to provide this token to all of our sub-components that want to do queries to our server. 代码重构变得更加困难了!

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

ClojureScript教程:输入回来的

回来的是一根魔杖,可以让你所有的国家梦想成真. Properly implemented, it provides a 状态-sharing abstraction that is safe, fast, 和 easy to use.

的 inner workings of 回来的 (和 the theory behind it) are somewhat outside the scope of this article. 而不是, 我将深入研究一个使用ClojureScript的工作示例, 这应该能在某种程度上证明它的能力!

在我们的语境中, 回来的 is implemented by one of the many ClojureScript libraries available; this one called 圆谎. It provides a Clojure-ified wr应用程序er around 回来的 which (in my opinion) makes it an absolute delight to use.

最基本的

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

  • 它是什么样子的
  • 它消耗什么数据
  • 它触发了什么Events

其余的都在幕后处理.

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

数据库

First things first: We need to decide what our 应用程序lication model is going to look like. 我们通过定义 形状 我们的数据,这些数据将在整个应用程序中被访问.

A good rule of thumb is that if the data needs to be used across multiple 回来的 components, 或者需要长期存在(就像我们的令牌一样), 然后应该存储在数据库中. 相比之下, if the data is local to the component (such as our username 和 password fields) then it should live as local component 状态 和 not be stored in the database.

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

(ns unearthing-clojurescript.状态.db
  (:要求[cljs.规范.[a]
            [圆谎.核心:as 圆谎]))

(s/def::token字符串?)
(s/def::db (s/keys: opt-un [::token]))

(def default-db
  {:令牌零})

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

  • 我们使用 Clojure的 规范 图书馆 to 描述 我们的数据应该是什么样子. 这在像Clojure[Script]这样的动态语言中尤其适用。.
  • 对于这个例子, we’re only keeping track of a global token that will represent our user once they have logged in. 这个令牌是一个简单的字符串.
  • 但是,在用户登录之前,我们没有令牌. 这是用 : opt-un 关键字,代表“可选的,非限定的”.(在Clojure中,常规关键字是这样的 :猫,而限定关键字可能是这样的 猫:动物/. Qualifying normally takes place at the module level—this stops keywords in different modules from clobbering each other.)
  • Finally, we 规范ify the default 状态 of our database, which is how it is initialized.

At any point in time, we should be confident that the data in our database matches our 规范 here.

Subscriptions

现在我们已经描述了我们的数据模型,我们需要反映我们的 视图 显示数据. We have already 描述d what our 视图 looks like in our 回来的 component—now we simply need to connect our 视图 to our database.

带回来的, we do not access our database directly—this could result in lifecycle 和 concurrency issues. 相反,我们将关系注册到数据库的一个方面 Subscriptions.

Subscriptions告诉reframe(和Reagent)我们依赖于数据库的一部分, 如果这部分被改变了, 那么我们的回来的组件应该被重新渲染.

Subscriptions的定义非常简单:

(ns unearthing-clojurescript.状态.潜艇
  (:要求[圆谎.核心:参考[reg-sub]]))

(reg-sub
  :token                         ; <- the name of the 潜艇cription
  (fn [{:keys [token] :as db} _] ; first argument is the database, second argument is any
    token))                      ; args passed to the 潜艇cribe function (not used here)

在这里,我们注册一个Subscriptions——对令牌本身. Subscriptions只是Subscriptions的名称, 以及从数据库中提取该项的函数. 我们可以对这个值做任何我们想做的, 和 mutate the 视图 as 多 as we like here; however, 在这种情况下, 我们只是从数据库中提取令牌并返回它.

有很多, more you can do with Subscriptions—such as defining 视图s on 潜艇ections of the database for a tighter scope on re-rendering—but we’ll keep it simple for now!

Events

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

  • 纯粹的Events(有 no 将新令牌写入数据库的副作用.
  • I/OEvents( 通过一些客户端交互出去请求我们的令牌的副作用.

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

(ns unearthing-clojurescript.状态.Events
  (:要求[圆谎.Core:参考[reg-event-db reg-event-fx reg-fx]:as rf]
            [unearthing-clojurescript.状态.Db:参考[default-db]])

; our start up event that initialises the database.
; we'll trigger this in our core.cljs
(reg-event-db
  : initialise-db
  [_] [_]
    default-db))

; a simple event that places a token in the database
(reg-event-db
  :存储登录
  (fn [db [_ token]]]
    (assoc db:token token))

同样,这里非常简单——我们定义了两个Events. 第一个用于初始化数据库. (看看它是如何忽略它的两个论点的? 初始化数据库时总是使用 default-db!)第二个是用于存储我们的令牌,一旦我们得到它.

Notice that neither of these Events have side effects—no external calls, no I/O at all! 这对于保持神圣的回来的进程的神圣性是非常重要的. 不要让它变得不纯洁以免你希望雷杜克斯的愤怒降临到你身上.

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

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

(reg-fx
  :请求令牌
  (fn [{:keys[用户名密码]}]
    (let [token (complicated-io-登录-operation username - password)]
      (rf/dispatch [:存储登录 token]))))

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

  • 第一个参数不再仅仅是数据库本身. It contains a multitude of other things that you can use for managing 应用程序lication 状态.
  • 第二个参数很像 reg-event-db.
  • 而不仅仅是归还新的 db, we instead return a map which represents all of the effects (“fx”) that should h应用程序en for this event. 在本例中,我们简单地调用 :请求令牌 效果,定义如下. 另一个有效的效果是 :调度,它只是调用另一个Events.

一旦我们的效果消散,我们的 :请求令牌 effect,它执行长时间运行的i /O登录操作. 一旦完成, 它愉快地将结果分派回Events循环, 这样循环就完成了!

ClojureScript教程:最终结果

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

(ns unearthing-clojurescript.登录
  (:要求[试剂.核心:作为试剂:参考[原子]]
            [圆谎.核心:作为rf])

;; -- STATE --

(def username (原子 nil))
(def password (原子 nil))

;; -- VIEW --

(defn组件
  []
  [: div
   [b“用户名”):
   [:input {:type "text"
            @用户名:价值
            :变化#(重置! username (-> % .—target .值)})
   [b“密码”):
   [:input {:type "password"
            :价值@password
            :变化#(重置! password (-> % .—target .值)})
   [:input {:type "button"
            :价值”登录!"
            :on-click #(rf/dispatch [:登录 {:username @username] 
                                             :密码@password]}}]])

我们的应用程序组件:

(ns unearthing-clojurescript.应用程序
  (:要求[unearthing-clojurescript.登录:作为登录]))
   
;; -- VIEW --

(defn组件
  []
  [: div
    [登录/组件]])

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

(let [token @(rf/潜艇cribe [:token])]
  ; ...
  )

把它们放在一起:

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

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

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

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

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

如果你已经准备好了,我建议你去看看 神奇的重新框架文件我们的简单工作示例. 我期待着阅读您对下面的ClojureScript教程的评论. 祝你好运!

了解基本知识

  • 什么是回来的状态?

    的 回来的 状态 refers to the single store that 回来的 uses to manage the 应用程序lication 状态. This store is solely controlled by 回来的 和 is not directly accessible from the 应用程序lication itself.

  • 回来的是Events源吗?

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

  • 什么是回来的容器?

    A 回来的 container (or simply a “container”) is a 反应 component that 潜艇cribes to the 回来的 状态, 当该部分状态发生变化时接收更新.

  • 回来的是一个框架吗?

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

  • 什么是ClojureScript?

    ClojureScript是一个针对JavaScript的Clojure编译器. It is commonly used to build web 应用程序lications 和 libraries using the Clojure language.

聘请Toptal这方面的专家.
现在雇佣

作者简介

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

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

输入您的电子邮件,即表示您同意我们的 隐私政策.

< div >

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

输入您的电子邮件,即表示您同意我们的 隐私政策.

Toptal开发者

加入总冠军® 社区.