喜欢英文的同学可以直接看 GitHub https://github.com/mvc-works/respo/wiki/Quick-Start
关于 Respo 之前有一篇初步介绍 https://segmentfault.com/a/1190000005061680
这篇文章主要是帮助你快速上手 Respo, 完整项目结构可以看:
https://github.com/mvc-works/respo-spa-example
希望借助这份文档你有能力写简单的 Respo Demo 出来
Respo 是基础 ClojureScript 实现的类 React 类库,
所以在开始之前, 你需要对 Clojure 的生态有了解, 并且理解 React.
命令行操作技能不可少, 教程基于 macOS, 其他平台命令行类似
另外这是一个网页, Chrome 开发技巧就不说了
关于 Clojure 可以看这里 https://wizardforcel.gitbooks.io/clojure-fpftj/content/1.html
关于 ClojureScript 入门可以看这个贴 http://clojure-china.org/t/clojurescript/330
开始之前你需要安装 Java 以及 Boot
可能很多人已经装了 Java, 但没有的话可以去 下载
Boot 是 Clojure 的一个项目管理工具, 可以brew install boot-clj
cljs 有个纯 REPL 的工具 Planck 也可以 brew install planck
React.js 基于 mutable data, 虽然引入不可变数据, 实际上只是混用
另外 ES6 开始大量引入新语法, 导致整体框架日渐复杂
很多奇怪的特性正在掩盖 React 核心的数据流, 是我不想看到的
Respo 是用纯 cljs 实现的一套 Virtual DOM, 可以渲染界面, 处理事件.
不过没有实现组件生命周期, 在功能上不如 React 丰富
Respo 当中事件处理也过于简单, 目前只做了基本的几个事件
定义组件通过 create-comp
函数, 例子如下:
(ns respo.component.space (:require [respo.alias :refer [create-comp div]])) (defn style-space [w h] (if (some? w) {:width w, :display "inline-block", :height "1px"} {:width "1px", :display "inline-block", :height h})) (defn render [w h] (fn [state mutate] (div {:style (style-space w h)}))) (def comp-space (create-comp :space render))
state
mutate
是从 render
里由类库传入的, 留意下, 后边用到
返回的 comp-space
其实也是一个函数, 可以在其他组件使用
组件名称 :space
会作在组件位置中使用, 所以都要写一下
一个 DOM 节点的表示方法和 React 在格式上并不一致
props 中的 style
和 event
被单独写, 方便程序处理
(input {:style style-input, :event {:input (on-text-state mutate)}, :attrs {:value state}})
Respo 当中使用高阶函数比较多, 函数式语言. 后面还会提到
create-comp
使用四个参数时可以对 state 进行编程
init-state
返回 {}
, update-state
对应 merge
按这样写的话, state 也可以用 Store 的手法进行抽象
(defn init-state [props] {:draft ""}) (defn update-state [old-state changes] (merge old-state changes)) (create-comp :demo init-state update-state render)
state 不是在组件 level 保存的, 而是在全局用一棵树存储
后面会看到应用初始化时会定义一个名为 states-ref
的 Atom
前面定义的组件是个函数可以直接调用, 和普通的函数写法一样
区别在于返回的数据中类型的结构分别对应组件或者 virtual DOM
一般组件明明会写个 comp-
前缀, 调用时直接用函数即可
(div {:style style-task} (comp-debug task {:left "160px"}) (button {:style (style-done (:done task))}) (comp-space 8 nil) (input {:style style-input, :event {:input (on-text-change props state)}, :attrs {:value (:text task)}}) (comp-space 8 nil) (div {:style style-time} (span {:style style-time, :attrs {:inner-text (:time state)}})))
组件参数的类型检查直接用 Clojure 里现成的比如 Spec 就好了
全局状态主要是指全局的 Store, states 虽然是全局但属于类库内部行为.
Clojure 里用 Atom 类型来存储 Store, 我写成 store-ref
:
(defonce store-ref (atom schema/store)) (defonce states-ref (atom {})) (defn dispatch [op op-data] (let [op-id (.valueOf (js/Date.)) new-store (updater @store-ref op op-data op-id)] (reset! store-ref new-store)))
Clojure 里用 reset!
对 Atom 类型进行修改
用 @store-ref
的语法来获得最新的数据, 不加 @
只能得到旧数据
Store 更新我用 updater
做的抽象, 模仿 Elm 当中的 updater
函数体代码主要用 case
来区分多个不同的 action 操作, 这里写成 op
:
(defn updater [store op op-data] (case op :inc (inc store) :dec (dec store) store))
Respo 提供了一个 render
函数将代码渲染到 DOM 上
这个 render
带有副作用, 函数名当初设计成 render!
也许更好
(defn render-app [] (let [mount-target (.querySelector js/document "#app")] (render (comp-container @store-ref) mount-target dispatch states-ref)))
注意 dispatch
函数是从 render 过程直接传入的
(defn -main [] (enable-console-print!) (render-app) (add-watch global-store :rerender render-app) (add-watch global-states :rerender render-app)) (set! (.-onload js/window) -main)
由于数据更新时页面要跟着更新, 就需要用 add-watch
监听
大致意思就是: 启动时渲染一次, 数据更新渲染一次
基于热替换的考虑, 我一般在 on-jsload
时也进行一次绘制
on-jsload
是在 boot-reload 插件处理 js 更新时自动调用的
(defn on-jsload [] (render-app))
编译代码需要 Boot, 相关的脚本可以直接使用,
完整的 build.boot
需要在前面的 GitHub 项目里查看:
(require '[adzerk.boot-cljs :refer [cljs]] '[adzerk.boot-reload :refer [reload]]) (deftask dev [] (comp (watch) (reload :on-jsload 'spa-example.core/on-jsload) (cljs) (target)))
其中的 'spa-example.core/on-jsload
需要改成对应的命名空间
网页在 assets/index.html
里, 细节要参考 build.boot
的配置,
配置完成可以启动 boot 进程, 打开 target/index.html
查看网页:
boot dev
监听事件的代码写在 :event
的表里边, 传入一个函数,
事件是经过 Respo 处理的, 从参数里 e
传进去,
state
和 mutate
是在组件 render 时从参数当中注入的
mutate
的行为类似 setState
:
(defn on-text-state [mutate] (fn [e dispatch] (mutate (:value e)))) (input {:style style-input, :event {:input (on-text-change props state)}, :attrs {:value (:text task)}})
向 Store 发送事件用到 dispatch
, 也是带有副作用的
(defn handle-remove [props state] (fn [event dispatch] (dispatch :remove (:id (:task props))))) (div {:style style-button, :event {:click (handle-remove props state)}} (span {:attrs {:inner-text "Remove"}}))
完整的代码可以在 GitHub 上 clone, 教程里并不完整
https://github.com/mvc-works/respo-spa-example遇到问题可以到 微博上找我 聊