July 20, 2019
By: Kevin

Reagent(React)深入学习第一部分

Clojurescript(cljs) 交互式执行环境

Reagent是React在cljs上的一个封装库, 诞生于React还是Class Component的时代.

在使用函数来组织组件这件事情上, 它走的比于Function Component更远, 使用方法也更加直观清晰.

cljs是一门有趣的语言, 这是Reagent系列的第一部分, 文中的代码都是可以现场编辑运行的(感谢klipse及其作者🙇).

一般的文章中的代码是这样的:

(map inc [1 2 3])
;(2 3 4)

而我们的是这样的:

(map inc [1 2 3])
;(2 3 4)

尝试个算法:

(def fib-seq-seq
  ((fn fib [a b]
     (lazy-seq (cons a (fib b (+ a b)))))
   0 1))
(take 13 fib-seq-seq)

还有UI:

(require '[reagent.core :as r]
         '[reagent.dom :as rdom])
(require '[cljsjs.highcharts])

[:button {:on-click #(js/alert "哈喽...")}
"我会说哈喽"]

我们使用了一个插件 clipse,来完成cljs代码的编译和执行, 这个插件默认是支持reagent的, 所以我们可以少写下面一行代码:

(rdom/render [:button {:on-click #(js/alert "哈喽...")}
"我会说哈喽"] js/klipse-container)

是不是很赞? 😄接下来我们看一下 reagent

Reagent 组件是一个函数

reagent组件是一个返回hiccup, hiccup是一个很简洁的把HTML表示为Data的方式, 以下面的代码为例:

<p>Hello world</p>

等价的hiccup:

[:p "Hello World"]

"Hello World" 组件

(defn greetings []
  [:p "Hello world"])

**动手:**上面的代码区域都是可以编辑的, 建议尝试下其他HTML组件, 比如h1 [:h1 "hahahaha"]

Hiccup

Hiccup可以完整的描述HTML, 标签, 属性

<img src="https://react.semantic-ui.com/logo.png">
[:img {:src "https://react.semantic-ui.com/logo.png"}]

**动手: ** 用reagent写一个标签<a href="https://github.com/reagent-project/reagent">Reagent</a>

hiccup的组织方式和html的组织方式是一样的, 层层嵌套, 就可以形成完成应用

[:div
  [:div "Hello world"]]

**注意: ** [[:div] [:div]]不是一个合法的组件, 必须有一个根, 新手总会碰到这个问题

[:div "p"
[:div]
[:div]]

这么做的坏处是多了一层额外的div, react 从某个版本引入了 fragments,就是用来解决这个问题

[:<>
 [:div]
 [:div]]

reagent会吧驼峰命名的属性, 转化成dashed命名方式: onClick -> on-click

[:button
 {:on-click
  (fn [e]
    (js/alert "你刚才按了我!"))}
 "来呀来呀~~"]

组件的样式同样也在这个map里

<p style="color: red; background: lightblue;">Such style!</p>

对应的

[:p
 {:style {:color "white"
          :background "darkblue"}}
 "我的 style!"]

可以用简写来简化代码:

[:div#my-id.my-class1.my-class2]

和下面的结构等价

[:div
  {:id "my-id"
  :className "my-class1 my-class2"}]

例子: SVG 太极图

(defn taiji []
  [:svg {:width 150 :height 150 :view-box "0, 0, 70, 70"}
   [:circle {:cx 35 :cy 35 :r 30 :stroke "rgb(0, 0, 0)" :stroke-width 4}]
   [:path {:fill "rgb(255, 255, 255" :d "M 35,65 C 18.43,65 5,51.57 5,35 5,18.43 18.43,5 35,5 L 35,35 Z M 35,65"}]
   [:path {:fill "rgb(0, 0, 0)" :d "M 35,5 C 51.57,5 65,18.43 65,35 65,51.57 51.57,65 35,65 L 35,35 Z M 35,5"}]
   [:path {:fill "rgb(0, 0, 0" :d "M 35,65 C 26.72,65 20,58.28 20,50 20,41.72 26.72,35 35,35 L 35,50 Z M 35,65"}]
   [:path {:fill "rgb(255, 255, 255" :d "M 35,5 C 43.28,5 50,11.72 50,20 50,28.28 43.28,35 35,35 L 35,20 Z M 35,5"}]
   [:circle {:fill "rgb(0, 0, 0" :cx 35 :cy 20 :r 6}]
   [:circle {:fill "rgb(255, 255, 255" :cx 35 :cy 50 :r 6}]])

**动手: **有心的同学们可以尝试一下红创的logo, 参照 SVG参考

例子: form

[:div
 [:h3 "你好呀...."]
 [:form
  {:on-submit
   (fn [e]
     (.preventDefault e)
     (js/alert
      (str "你说: " (.. e -target -elements -message -value))))}
  [:label
   "说点啥:"
   [:input
    {:name "message"
     :type "text"
     :default-value "Hello"}]]
  [:input {:type "submit"}]]]

嵌套

前文已经多次提到, 组件的威力在于嵌套.

reagent的组件是一个函数, 而函数是可以调用的, 我们可以直接调用它, 获得结果.

(defn greetings []
  [:p "Hello world"])
(greetings)

很快我们就会知道这不是个好习惯(参照官方解释 用中括号而不是圆括号), 我们不会直接去call一个compnent function, 而是使用hiccup嵌套的方式.

(defn greet2 [message]
 [:div [greetings]])

(defn many-taijis []
  (into
   [:svg {:style {:border "1px solid"
                  :background "white"
                  :width "600px"
                  :height "600px"}}]
   (for [i (range 12)]
     [:g
      {:transform (str
                   "translate(300,300) "
                   "rotate(" (* i 30) ") "
                   "translate(100)")}
      [taiji]])))

简单来说, 如果使用圆括号, 就直接是函数调用了, 而hiccup的形式, 我们把调用执行的权力交给了reagent, reagent会根据参数是否需要再次调用还是使用以前的cache

本质是是个性能问题, 从效果上, 两者没有差异

状态的保持和修改

能做事情的组件才是好组件, 一个好组件需要

  1. 初始化, 初始化的数据决定了最初的组件形态
  2. 对用户的操作作出回应
  3. 关注外部变化, 对变化作出反应

前文提到组件就是个函数, 输入就是函数的参数,Reagent为组件提供了关注并且响应外部变化的方式: reagent.core/atom

Reagent的atom和clojurescript自己的atom非常相似, 我们使用swap!, reset!函数来改变atom的值, 也用defef,或者@来拿到atom的值

Reagent的Atom特殊性在于, 如果这个Atom在某个组件内deref, Atom的数据更新会触发这个组件的更新

例子: Atom驱动组件显示

(def c
  (r/atom 1))

(defn counter []
  [:div
   [:div "当前计数器的值: " @c]
   [:button
    {:disabled (>= @c 4)
     :on-click
     (fn clicked [e]
       (swap! c inc))}
    "增加"]
   [:button
    {:disabled (<= @c 1)
     :on-click
     (fn clicked [e]
       (swap! c dec))}
    "减少"]
    (into [:div] (repeat @c [taiji]))])

例子: ATOM驱动显示不同组件

(let [show? (r/atom false)]
  (fn waldo []
    [:div
     (if @show?
       [:div
        [:h3 "找到了!"]
        [:img
         {:src "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1563712732907&di=62106c9c37d07047ea5be4c0aad7ca86&imgtype=0&src=http%3A%2F%2Fimg1.tplm123.com%2F2008%2F10%2F08%2F629%2F3520859682796.jpg"
          :style {:height "320px"}}]]
       [:div
        [:h3 "搜索一只小熊猫..."]
        [:img
         {:src "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1563712441742&di=13f832585f86c98789c0355a4506e167&imgtype=0&src=http%3A%2F%2Fww1.sinaimg.cn%2Flarge%2F9519feb6gw1f5dou9gayqj20hs0hsdm7.jpg"
          :style {:height "320px"}}]])
     [:button
      {:on-click
       (fn [e]
         (swap! show? not))}
      (if @show? "找到了" "搜索")]]))

数据是最灵活的, 数据驱动组件呈现. 为了订阅模式, Reagent提供了atom, reaction, cursorstrack来实现订阅的各种场景, 即使不用re-frame, atom也可以应付相当复杂的场景.

(def rolls (r/atom [1 2 3 4]))
(def sorted-rolls (reagent.ratom/reaction (sort @rolls)))

(defn sorted-reaction []
  [:div
   [:button {:on-click (fn [e] (swap! rolls conj (rand-int 20)))} "来一个数字"]
   [:p (pr-str "升序:" @sorted-rolls)]
   [:p (pr-str "降序:" (reverse @sorted-rolls))]])

cursortrack的使用场景后续补充

Tags: Reagent react clojurescript