Respo 确实是个轮子, 甚至不像是 react-lite
那样能替代 React
Respo 主要的目标是用 ClojureScript 重新实现一遍, 以及改进和学习
为了方便使用, 我把相应代码整理出一个模块, 方便的有兴趣的同学使用
https://github.com/mvc-works/respo-spa
随后我增加了一个 example 用来展示具体的使用方法
https://github.com/mvc-works/respo-spa-example
我用一个视频录制了从创建项目到完成界面的过程
http://www.tudou.com/programs/view/njte4UfduKw/
了解 cljs 的同学可以直接看 repo 里的内容, 代码实际上挺短了
具体的 API 在 respo
和 respo-client
两个 repo
在 respo-spa
当中主要是封装出来比较友好的接口方便上手
这篇文章相当于一个文字版, 我介绍一下 respo-spa
的用法
首先看一下 Respo 用法上和 React 一些比较明显的区别:
states 是全局的, 不是 React 那样私有状态, 这有利于热替换
Respo 当中 element 的 DSL 分成
style event attrs
三类
写起来有点长. 但是对于框架有好处, 需要的话自己可以再封装Respo 中 state 和 Store 一样, 可以自己设置类型, 以及如何更新
Respo 中 mutate 是通过函数式的写法模拟
setState
, 有区别
...此外 Respo 中缺少很多 React 中有的生命周期方法, 少了很多功能
毕竟不是文档, 我从源码开始分解吧, example 涉及了大部分功能了:button.cljs
:
(ns spa-example.component.button
(:require
[respo.alias :refer [create-comp div span]]
[hsl.core :refer [hsl]]))
; 定义无状态的组件,
; 第一个函数参数对应 props
(defn render [text on-click]
; 第二个函数是 state 和修改 state 的函数
(fn [state mutate]
; 注意这里, style event 是分开的, 所以是两层的 map
(div {:style {:background-color (hsl 200 80 70)
:display "inline-block"
:padding "0 8px"
:color "white"
:margin "0 8px"
:cursor "pointer"}
:event {:click on-click}}
; 属性是 inner-text, 内部翻译为 innerText
(span {:attrs {:inner-text text}}))))
; 创建组件的写法, 没有 state, 因而参数较少
(def comp-button (create-comp :button render))
box.cljs
:
(ns spa-example.component.box
(:require
[respo.alias :refer [create-comp div span]]
[spa-example.component.button :refer [comp-button]]))
; 定义状态, 这里直接用来数字
(defn init-state [] 0)
; 状态每次操作, 直接加上一个数字
(defn update-state [state step] (+ step state))
; 处理事件, 其中 dispatch 由框架传递, mutate 由参数传递
(defn handle-click [mutate step]
(fn [simple-event dispatch]
; mutate 会调用到 update-state, 第一个参数自动加上
(mutate step)))
; 可以自己封装函数用来简化纯文本的 span
(defn text [x]
(span {:attrs {:inner-text (str x)}}))
(defn render [n]
(fn [state mutate]
(div {}
(text (str n ". "))
; 调用组件的语法, 就和函数一样用
(comp-button "inc" (handle-click mutate n))
(text state))))
; 创建带状态的组件, 注意参数数量增加了
(def comp-box (create-comp :box init-state update-state render))
container.cljs
:
(ns spa-example.component.container
(:require
[respo.alias :refer [create-comp create-element div span]]
[spa-example.component.button :refer [comp-button]]
[spa-example.component.box :refer [comp-box]]))
; Respo 内部没有定义足够多元素, 可以自己绑定
(defn hr [props & children]
(create-element :hr props children))
(defn handle-click [simple-event dispatch]
(dispatch nil nil))
(defn render [store]
(fn [state mutate]
(div {}
; 列表类型的元素渲染, 外边包裹 div, 里边用 sorted-map
(div {}
(->> (range 10)
(map-indexed (fn [index n]
[index (comp-box n)]))
(into (sorted-map))))
(hr {})
(comp-button "inc" handle-click)
(span {:attrs {:inner-text (str store)}})
(div {}
(comp-box)))))
(def comp-container (create-comp :container render))
core.cljs
:
(ns spa-example.core
(:require [respo-spa.core :refer [render]]
[spa-example.updater.core :refer [updater]]
[spa-example.component.container :refer [comp-container]]))
; 定义全局的 store 的引用
(defonce store-ref (atom 0))
; 定义全局的 states 的应用
(defonce states-ref (atom {}))
; dispatch 用来操作 store, 用的函数是 reset! 是有副作用的
(defn dispatch [op op-data]
(reset! store-ref (updater @store-ref op op-data)))
(defn render-app []
(let [target (.querySelector js/document "#app")]
; render 函数来自 respo-spa 的封装, 强制传递一些参数过去
(render (comp-container @store-ref) target dispatch states-ref)))
(defn -main []
(enable-console-print!)
(render-app)
; 在 store 和 states 发生改变是重新绘制界面
(add-watch store-ref :changes render-app)
(add-watch states-ref :changes render-app)
(println "app started!"))
(defn on-jsload []
; 在 js 代码热替换之后重新绘制界面
(render-app)
(println "code updated."))
(set! (.-onload js/window) -main)
updater/core.cljs
只是说明一下可以定制:
(ns spa-example.updater.core)
(defn updater [store op op-data]
(inc store))
整体上都还是我之前在 React.js 当中用的那些套路, 只是更纯粹一些
代码的稳定性还有待时间, 毕竟使用的项目太少, 完全不能保障
生命周期方法的缺失大概会影响具体的使用, 只是程度现在不确定
同时 mutate 函数实际上印象性能, 还有待优化, 我现在并没有处理
视频部分非常详细了, 有兴趣可以照着试试看玩 cljs, 有问题微博上问我