使用 Reagent 构建单页应用

来源:开源中国社区 作者:oschina
  

背 景

最近我开始做一个与UI组件息息相关的新工程。我断定,这是一个绝佳的机会去学习Angular和React,从而打造一个简单页面应用的客户端。

经过再三评估后,我决定,React更加适合这个工程。尤其是,我发现虚拟DOM的主意非常具有吸引力,它的组件作为一个基础方法来管理应用状态。

我曾经更加深入地使用React,发现它在许多领域有缺陷。比如说,对于复杂数据的绑定。它没有提供合适的方案,当有一些例如react-forms库,我发现它们并没有满足我的需求。

据说Om有诸多巨大优势,我在思考,我应该重新审视ClojureScript。我之前做过一些有关ClojureScript的工程项目,我总是最终绕回到JavaScript。

对于我来说,(它的)便捷之处不足以超越JavaScript的成熟度与它的可用工具。我发现其中一点是,调试生成的JavaScript(代码)是非常痛苦的。通过添加源映射,这个问题已经得到解决了。

尝试Om

我翻遍Om教程,发现Om向用户公开了大量附带的细节。我还遇到了Om存在的少数几个不足,分别是必须传递nil参数、具体化的协议和使用#js提示符手动转换成js等。值得指出的是,尽管Prismatic中的 om-tools 

总的来看,我感觉要使Om的生产效率更高就需要大量的时间投资。我自己想要一种更高层次的抽象来创建UI部件并跟踪其状态。因此我尝试使用Reagent。该库提供了一种非常直观的模型,用于装配UI部件并跟踪其状态,并且你只需要学习很少的概念就可以开始高效地使用它。

Om和Reagent之间的区别

Om和Reagent各自有自己的优势和劣势,分别可以做出不同的设计决策,从而导致对它们有不同的权衡取舍。至于哪个库更好主要取决于你所要解决的问题。

Om和Reagent之间的最大不同在于Om对状态管理具有高度说明性以确保部件的可重用性。使用Om部件直接操作全局状态或通过函数调用来操作是一种反模式。相反,我们希望部件之间使用 core.async通道进行通信。这样做的目的是确保部件的高度模块化。Reagent把设计决策留给用户并允许用户根据需要使用全局和本地状态的组合。

Om以世界中心为论点,探讨数据时如何被渲染的。它把React DOM与Om组件作为实施细节。这个决定经常导致冗长的细节暴露给用户。这些明显是抽象的,但并不是旨在提供这样一个抽象(对象),你必须编写自己的提供者,作为棱柱和光学工具参考对象。

另一方面,响应式提供一个标准方式来定义UI组件,使用DOM表示的打嗝式语法。每一个UI组件就是一个数据结构,代表一个特定的DOM元素。以DOM为中心的UI视图,响应式可使用组合的简单、直观的UI组件。由此产生的代码是非常简洁的、可读性强的。值得注意的是,没有什么可以阻止您在自定义组件中交换。唯一的限制是,该组件返回的东西是可用的。

采用响应式

这篇文章的剩余部分将会构建一个琐碎的响应式应用程序,我希望解析明白为什么响应式是如此优秀的一个库。不同种类的CRUD 应用程序可能是时下最常见的web应用程序。让我们来看下,如何使用一些模块来创建一个简单表单,并且我们将会手机与发送给服务器。

本文中我不会去详细介绍如何去设置一个ClojureScript工程,不过你还是可以使用 reagent-example 项目来了解这个过程。这个工程需要有 Leiningen 构建工具,而你将需要在继续之前先将其安装好。

当你检出了这个工程之后,就需要通过运行 lein cljsbuild auto 命令来启动 ClojureScript 编译器,然后使用 lein ring server 来启动服务器。

应用包含了一些被绑定到模型的UI组件。无论用户在何时改变了一个组件的值,修改都会被反映到我们的模型中。当用户点击了提交按钮,那么当前的状态就会被发送到服务端。

ClojureScript 可以在 src-cljs源代码目录下的main.core中的被找到。让我们删除它的内容并且从头开始编写我们的应用程序。作为第一个步骤,我们将需要在我们的命名空间定义中对 reagent 进行引用。

1
(ns main.core (:require [reagent.core :as reagent :refer [atom]]))

接下来,让我们创建一个 Reagent 组件来呈现我们页面的容器。

1
2
3
(defn home []
  [:div
    [:div.page-header [:h1 "Reagent Form"]]])

现在我们就能够通过调用 render-component函数来在页面上渲染出这个组件。

1
2
(reagent/render-component [home]
  (.getElementById js/document "app"))

如我前面所提到的,组件可以嵌套到另外一个组件里面。为了向我们的表单中添加一个文本域,我们将编写一个函数来呈现它并且将它添加到我们的home组件中。

1
2
3
4
5
6
7
8
9
(defn text-input [label]
  [:div.row
    [:div.col-md-2
      [:span label]]
    [:div.col-md-3
      [:input {:type "text" :class "form-control"}]]])(defn home []
  [:div
    [:div.page-header [:h1 "Reagent Form"]]
    [text-input "First name"]])

注意尽管text-input是一个我们并没有调用到的函数,不过我们会把它放到一个向量中。这样做的原因是我们在指定组件的层级。组件在它们需要被渲染时将会由Reagent来运行。

我们可以轻松地将行提取到一个单独的组件中去。这里我们还是不会需要直接调用到 row 函数,不过是将组件看做是数据,并且在当它需要被计算时,把它留给 Reagent 来处理。

1
2
3
4
5
(defn row [label & body]
  [:div.row
   [:div.col-md-2 [:span label]]
   [:div.col-md-3 body]])(defn text-input [label]
  [row label [:input {:type "text" :class "form-control"}]])

现在我们有了一个输入域来进行显示。接下来我们需要创建一个模型,并且将我们将我们的组件绑定到它上面。 Reagent 允许我们使用它的在React状态之上的原子抽象来做这件事情。Reagent 的原子行为就像标准的 Clojure原子一样。主要的不同之处在于,原子的值里面的一个改变会导致任何对其进行了解引用的组件被重绘。

每当我们希望创建一个本地或者全局状态的时候,就创建一个原子来持有它。这样就拥有了一个简单的模型,我们可以在其中创建状态的变量并且观察到他们随着时间的变化。让我们添加一个原子来持有应用程序的状态,以及一对可以对其进行访问和更新处理函数。

1
2
3
4
(def state (atom {:doc {} :saved? false}))(defn set-value! [id value]
  (swap! state assoc :saved? false)
  (swap! state assoc-in [:doc id] value))(defn get-value [id]
  (get-in @state [:doc id]))

现在当 onChangeevent被调用时,我们就可以通过更新 text-input组件来设置状态并且显示当前状态的value了。

1
2
3
4
5
6
7
8
9
(defn text-input [id label]
  [row label   [:input
     {:type "text"
       :class "form-control"
       :value (get-value id)
       :on-change #(set-value! id (-> % .-target .-value))}]])(defn home []
  [:div
    [:div.page-header [:h1 "Reagent Form"]]
    [text-input :first-name "First name"]])

让我们在表单中添加一个保存按钮,那样我们就可以将状态进行持久化。目前,我们会简单地将当前状态在控制台上进行日志记录。

(defn home []
  [:div
    [:div.page-header [:h1 "Reagent Form"]]
    [text-input :first-name "First name"]    
    [:button {:type "submit"
              :class "btn btn-default"
              :on-click #(.log js/console (clj->js @state))}
     "Submit"]])

如果我们打开控制台,那么我们应该就会看到当点击提交按钮是,:first-name键的当前值被填充到文档中。现在我们能轻松地为姓氏添加第二个组件,并且看到它已经被使用完全一样的方式绑定到模型上了。

1
2
3
4
5
6
7
8
9
10
11
(defn home []
  [:div
    [:div.page-header [:h1 "Reagent Form"]]
 
    [text-input :first-name "First name"]
    [text-input :last-name "First name"]
 
    [:button {:type "submit"
              :class "btn btn-default"
              :onClick #(.log js/console (clj->js @state))}
     "Submit"]])

目前为止我们已经使用了一个全局变量来持有所有的状态,而对于小型的应用程序而言比较方便的方法并不能进行很好的扩展。幸运的是,Reagent 允许我们的组件中拥有本地化的状态。让我们来看看如何实现一个多选组件并且观察其运行。

当用户点击列表中的一项时,我们会想要将其标识为已选。很明显,这只是跟列表组件相关的意见事情而并不应该被全局地进行跟踪。我们要做的就是创建一个本地状态并且一个闭包中进行初始化。

我们实现的多选,将会创建一个组件来呈现列表,以及另外一个组件来呈现每一个被选中的项。这个列表组件将会接收一个id以及一个跟在选项后面的标识。

每一项都会由一个包含了选项的id和值向量来呈现,例如:[:beer "Beer"]。列表的值则有当前已选项的id集合来呈现。

我们将会使用一个let绑定来用一个id到每一项当前状态的映射对一个原子进行初始化。

1
2
3
4
5
6
7
8
9
(defn selection-list [id label & items]
  (let [selections (->> items (map (fn [[k]] [k false])) (into {}) atom)]    
    (fn []
      [:div.row
       [:div.col-md-2 [:span label]]
       [:div.col-md-5
        [:div.row
         (for [[k v] items]
          [list-item id k v selections])]]])))

单项组件将会负责当有点击并且将列表的新值持久化到文档中时更新其状态。

1
2
3
4
5
6
7
8
9
(defn list-item [id k v selections]
  (letfn [(handle-click! []
            (swap! selections update-in [k] not)
            (set-value! id (->> @selections                                (filter second)
                                (map first))))]
    [:li {:class (str "list-group-item"
                      (if (k @selections" active"))
          :on-click handle-click!}
      v]))

让我们向表单中添加选择列表的一个实例,并对其进行观察。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(defn home []
  [:div
    [:div.page-header [:h1 "Reagent Form"]]
 
    [text-input :first-name "First name"]
    [text-input :last-name "First name"]
 
    [selection-list :favorite-drinks "Favorite drinks"
     [:coffee "Coffee"]
     [:beer "Beer"]
     [:crab-juice "Crab juice"]]
 
    [:button {:type "submit"
              :class "btn btn-default"
              :onClick #(.log js/console (clj->js @state))}
     "Submit"]])

最后,让我们对提交按钮进行一下更新,使其能实际向服务器发送数据。我们将使用 cljs-ajax 库莱处理Ajax调用。让我们向 project.clj 添加如下的依赖 [cljs-ajax "0.2.6"]并且对我们的命名空间进行更新来引用到它。

1
2
(ns main.core (:require [reagent.core :as reagent :refer [atom]]
           [ajax.core :refer [POST]]))

这件事情做好之后我们就能编写一个 sava-doc函数,它将会将文档的当前状态发送给服务端,并且将状态设置为成功。

1
2
3
4
(defn save-doc []
  (POST (str js/context "/save")
        {:params (:doc @state)
         :handler (fn [_] (swap! state assoc :saved? true))}))

现在我们就能通过更新表单来显示一条能提示说文档已经被保存的消息或者是基于我们的状态原子中:saved?键的值的提交按钮。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
(defn home []
  [:div
    [:div.page-header [:h1 "Reagent Form"]]
 
    [text-input :first-name "First name"]
    [text-input :last-name "Last name"]
    [selection-list :favorite-drinks "Favorite drinks"
     [:coffee "Coffee"]
     [:beer "Beer"]
     [:crab-juice "Crab juice"]]
 
   (if (:saved? @state)
     [:p "Saved"]
     [:button {:type "submit"
              :class "btn btn-default"
              :onClick save-doc}
     "Submit"])])

在服务端我们会简单地对由客户端提交的值进行记录并且返回“ok”。

1
2
3
4
5
6
7
(ns reagent-example.routes.services  (:use compojure.core)
  (:require [reagent-example.layout :as layout]
            [noir.response :refer [edn]]
            [clojure.pprint :refer [pprint]]))(defn save-document [doc]
  (pprint doc)
  {:status "ok"})(defroutes service-routes  (POST "/save" {:keys [body-params]}
        (edn (save-document body-params))))

当路由在我们的处理器中挂好以后,我们应该就可以看到当我们从客户端提交一条消息时会法神什么了:

1
{:first-name "Jasper", :last-name "Beardly", :favorite-drinks (:coffee :beer)}

如你所见,上手 Reagent 极其容易,只需要非常少的代码来创建一个工作应用程序。你会说单页面 Reagent应用实际上只适合一个单独的页面。 :) 接下来我们就会看看如何使用 secretary 库来向应用程序中添加客户端路由。

本文转自:开源中国社区 [http://www.oschina.net]
本文标题:使用 Reagent 构建单页应用
本文地址:
http://www.oschina.net/translate/building-single-page-apps-with-reagent
参与翻译:
leoxu, xufuji456, 昌伟兄

英文原文:BUILDING SINGLE PAGE APPS WITH REAGENT


时间:2016-08-12 08:45 来源:开源中国社区 作者:oschina 原文链接

好文,顶一下
(0)
0%
文章真差,踩一下
(0)
0%
------分隔线----------------------------


把开源带在你的身边-精美linux小纪念品
无觅相关文章插件,快速提升流量