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, 有问题微博上问我