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
本质是是个性能问题, 从效果上, 两者没有差异
状态的保持和修改
能做事情的组件才是好组件, 一个好组件需要
- 初始化, 初始化的数据决定了最初的组件形态
- 对用户的操作作出回应
- 关注外部变化, 对变化作出反应
前文提到组件就是个函数, 输入就是函数的参数,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, cursors和 track来实现订阅的各种场景, 即使不用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))]])
cursor和track的使用场景后续补充