Rum - 一个简单,灵活,可扩展的ClojureScript的React实现
Table of Contents
使用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实现了 IAtom
和 IWatchable
, 接口的实现使其可以替换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)