狗好看の世界

Less words, more attempting.

Rum - 一个简单,灵活,可扩展的ClojureScript的React实现

使用Rum

  • 添加依赖 [rum "0.6.0"]
  • (require '[rum.core :as rum])

简单的定义组件, 初始化, 并挂载到页面上.

(ns example
  (:require [rum.core :as rum]))

(rum/defc label [n text]
  [:.label (repeat n text)])

(rum/mount (label 5 "abc") js/document.body)

Rum API

Rum提供一个宏 defc (define component的缩写):

(rum/defc name doc-string? [< mixins+]? [params*] render-body+)

defc 定义一个顶级函数接受一组参数 params*, 返回一个会按照 render-body 中指定方式渲染的React组件.

defc 的实现原理如下:

  • 定义一个render函数, 把 render-body 显式的放入 do 中, 并用 sablono.core/html 宏来渲染.
  • 通过render函数, 和指定的mixin来创建一个React类.
  • 通过这个类, 生成构造函数 fn [params] -> ReactElement
  • 定义一个顶级的变量 name , 指向构造函数.

当调用的时候, name 函数会创建一个React元素. params 参数会被传入, 所以在 render-body 中可以被使用.

rum.core/mount 来挂载这个组件:

(run/mount element dom-node)

mount 会返回被挂载的元素, 使用同样的参数多次调用 mount 是安全的.

需要注意的是, mount 并不会让组件可以自动被更新, 如果有必要, 需要通过代码或者mixin来实现. 可以通过再次挂载的方式:

(add-watch state :render
  (fn [_ _ _ _]
    (rum/mount element node)))

或者调用 request-render 函数:

(let [component (rum/mount element dom-node)]
  (add-watch state :render
    (fn [_ _ _ _]
      (rum/request-render component))))

request-render 并不会立刻执行渲染, 而是将你的组件放入渲染队列, 在 requestAnimationFrame 回调时, 进行渲染. 需要重新渲染时, request-render 是更推荐的方式.

Mixins

Rum提供了一些mixins用来模拟 quiecent, om, reagent 中的一些功能. 编写自己的mixin也是非常容易的.

rum.core/static 会检查组件的参数是否变化过(按照Clojure的 -equiv 语意), 如果没有产生变化,就避免重绘:

(rum/defc label < rum/static [n text]
  [:.label (repeat n text)])

(rum/mount (label 1 "abc") body)
(rum/mount (label 1 "abc") body)        ; 不会重绘
(rum/mount (label 1 "xyc") body)        ; 会重绘

rum.core/local 创建一个atom作为组件级的状态. 当你使用 swap! 或者 reset! 修改这个atom的时候, 组件会被自动重绘. 可以通过 :rum/local 这个key来获取到这个atom:

(rum/defcs stateful < (rum/local 0) [state title]
  (let [local (:rum/local state)]
    [:div
     {:on-click (fn [_] (swap! local inc))}
     title ": " @local]))

(rum/mount (stateful "Clicks count") js/document.body)

注意, 我们使用 defcs 代替 defc , 会在 render 的第一个参数得到state. 另外需要注意, rum.core/local 并不是一个mixin的值, 而是一个生成器函数. 它接受一个初始值,返回一个mixin.

rum.core/reactive 会创建一个"反应性"的组件, 它会追踪所有在 render 函数中的引用. 在这些引用的值改变的时候, 会自动更新组件:

(def color (atom "#cc3333"))
(def text (atom "Hello"))

(rum/defc label < rum/reactive []
  [:.label {:style {:color (rum/react color)}}
   (rum/react text)])

(rum/mount (label) js/document.body)
(reset! text "Good Bye")                ; 会造成重绘
(reset! color "#000")                   ; 同样重绘

rum.core/react 函数的功能, 在这个例子中就相当于 deref, 并且会观察这些引用.

最后, rum.core/cursored, 会追踪所有被作为参数传入的引用:

(rum/defc label < rum/cursored [color text]
  [:.label {:style {:color @color}} @text])

需要注意, cursored mixin, 创建被动的组件: 对于它自己对引用产生的变化, 不会有反应. 而且它只会在被父组件重新创建的时候, 比较这些参数的值. 另外, rum.core/cursored-watch mixin会对参数表中, 所有 IWatchable 的参数进行变化的观察.

(rum/defc body < rum/cursored rum/cursored-watch [color text]
  (label color text))

(rum/mount (body color text) js/document.body)

(reset! text "Good bye")                ; 会重绘body和label
(reset! color "#000")                   ; 同样的

Rum也提供cursor, 可以将atom树中的子树, 作为类似atom的结构提供出来:

(def state (atom {:color "#cc3333"
                  :label1 "Hello"
                  :label2 "Goodbye"}))

(rum/defc label < rum/cursored [color text]
  [:.label {:style {:color @color}} @text])

(rum/defc body < rum/cursored rum/cursored-watch [state]
  [:div
   (label (rum/curosr state [:color]) (rum/cursor state [:label1]))
   (label (rum/curosr state [:color]) (rum/cursor state [:label2]))])

;; 只会重绘第二个label
(swap! state assoc :label2 "Good bye")

;; 两个label都会被重绘
(swap! state assoc :color "#000")

;; cursor可以像atom一样,被swap!和reset!
(reset! (rum/cursor state [:label1]) "Hi")

cursor实现了 IAtomIWatchable , 接口的实现使其可以替换atom中对应的内容. cursor可以被使用在 reactive 的组件中.

Rum之美在于你可以自己组合mixin, 在UI树中, 你可以使用完全不同的类. 你可以在顶级使用 reactive 组件, 里面是 static 的组件, 接着是每一秒都更新的组件, 再里面是一个 cursored 组件. 强大而简单.

Rum 组件模型

rum会定义自己的类和组件, 在里面包含了React的类和组件.

rum中的每一个组件, 都有一个关联的state. state的结构就是一个CLJS的map:

  • :rum/react-component 对应React的组件/元素
  • :rum/id 唯一的组件ID
  • 所有的mixin都会使用他们内部的标记
  • 任何你放入这里的东西(可以随意将任何东西放到这个里面)

??? 这里并不是很清楚 当前状态的引用保存在使用 volatile! 打包的值, 放在 state[":rum/state"]. 高效的state是可变的, 但是组件中并不会直接修改他们, 取而代之, 通过所有的生命周期方法来返回state.

类定义了组件的行为, 包括render函数, 类是通过mixin来创建的.

mixin是构建其他组件的基础组件, 每一个mixin都是一个map, 里面包含一下的一个或者多个函数.

{ :init                 ;; state, props     ⇒ state
  :will-mount           ;; state            ⇒ state
  :did-mount            ;; state            ⇒ state
  :transfer-state       ;; old-state, state ⇒ state
  :should-update        ;; old-state, state ⇒ boolean
  :will-update          ;; state            ⇒ state
  :render               ;; state            ⇒ [pseudo-dom state]
  :wrap-render          ;; render-fn        ⇒ render-fn
  :did-update           ;; state            ⇒ state
  :will-unmount         ;; state            ⇒ state 
  :child-context        ;; state            ⇒ child-context }

为组件的类定义任意的属性和方法, 使用 :class-properties map:

{:class-properties { ... }}

想象一下, 一个类通过N个mixin构成, 当React中的生命周期事件产生的时候(例如: ComponentDidMount), 所有的 :did-mount 会被按照mixin出现的从前到后的顺序被调用. 通过他们来返回初始化的状态. 类似的, 多个组件中的 context map会被合并成一个.

render的模型不同, 这里必须只有一个单独的 :render 函数, 接受state, 返回dom的vector和新的状态. 如果mixin想修改render的行为, 它应该提供 :wrap-render 函数, 这些函数会被从左到右调用. 举例, :render 被作为第一个参数传入 :wrap-render 函数, 返回的结果传入下一个的 :wrap-render 函数.

编写自己的mixin

一个简单的, 强制每一秒都刷新的mixin:

(def autorefresh-mixin
  {:did-mount (fn [state]
                (let [comp (:rum/react-component state)
                      callback #(rum/request-render comp)
                      interval (js/setInterval callback 1000)]
                  (assoc state ::interval interval)))
   :transfer-state (fn [old-state state]
                     (merge state (select-keys old-state [::interval])))
   :will-unmount (fn [state]
                   (js/clearInterval (::interval state)))})

(rum/defc timer < autorefresh-mixin []
  [:div.timer (.toISOString (js/Date .))])

把所有的东西组合到一起.

假设你有一个简单的render函数:

(defn render-label [text]
  (sablono.core/html
   [:div.label text]))

你创建一个mixin, 把它转换成一个React组件:

(def label-mixin
  {:render (fn [state]
             [(render-label (:text state)) state])})

然后你通过一个单独的mixin来创建React类:

(def label-class (rum/build-class [label-mixin] "label-class"))

然后定义一个简单的wrapper来把这个类转化成React元素:

(defn label-ctor [text]
  (rum/element label-class {:text text} nil))

最后你调用ctor来获得一个元素的实例, 并把它挂载在页面的某个位置:

(rum/mount (label-ctor "Hello") js/document.body)

这个是rum中发生的每一步的细节, 通过使用 rum/defc, 所有的事情都被压缩成了一步.

(rum/defc label [text]
  [:div.label text])

(rum/mount (label "Hello") js/document.body)

Comments