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)