shadow-cljs介绍
什么是shadow-cljs
shadow是cljs编译工具, 提供了极佳的cljs开发体验, 它帮我们解决了以下问题:
- cljs项目编译/配置
- npm集成
- 快速编译cljs, 以及已经编译的cljs代码的cache
- 多种编译目标的支持: :browser, :node-script, :npm-module, :react-native, :chrome-extension
- 代码(cljs, css)的热更新(hot-relaod)
- 自带cljs的repl
- 代码分包(coce splitting)
特别是npm集成的问题, shadow使cljs和npm生态紧密联系在了一起. 我们的前端开发(ReactJS)和移动端开发(ReactNative), 都是基于shadow的.
现在cljs的官方在最新的1.10.741 release中也完成了对nodejs中require的支持. 思路上是cljs代码编译的目标可以使用webpack和metro深度结合. 值得我们继续关注. 但当前shadow还是个更优的选择.
配置
target
支持: :browser, :node-script, :npm-module, :react-native, :chrome-extension
- :browser 最常用, 用于可以执行在浏览器的应用.
- :react-native 提供了对于rn的支持.
shadow-cljs的热更新
热更新是个特性, 要支持这个特性, 需要把数据 (data)和展现 (View)完全分开, 需要数据中心化的存储, 而不能散在各处.
在cljs的开发中, 我们使用re-frame来集中管理状态 (数据状态/路由状态), 使用React来做展现. 是满足这一条件的.
React从架构上来说: V = f(D), 展现完全由数据驱动.
每次刷新的时候, 只要数据的保留, View是能够完全重新render的.
回到shadow-cljs, 在启动shadow-cljs watch app的时候, shadow负责增量的把修改的cljs代码编译为js. 并且开始reload这些js代码, 下文的生命周期部分会对这个机制进行详细的解释.
特别值得注意: js中执行着一个cljs的runtime (repl-server), 我们在开发端的 (repl-client)中进行动态的eval后, 这些代码也会通过websocket加载到cljs的runtime, 即时生效. 作为lisp的独门绝技, 可以极大增加开发的乐趣和效率.
+---------------+ +-------------------+
| | | js/runtime |
| core.cljs | shadow-cljs watch | |
| util.cljs +---------------------->| app.js |
| events.cljs | repl | |
| | | |
+---------------+ +-------------------+
生命周期
- init: 执行一次, 在start前执行, 做好初始化工作.
- start: 重新render整个app, 每次hotload后都需要调用.
- stop: 这个是可选的, 每次hotreload之前调用, 用来清理一些临时状态.
(ns example.app)
(defn ^:dev/after-load start []
(js/console.log "start"))
(defn init []
(js/console.log "init")
(start))
;; optional
(defn ^:dev/before-load stop []
(js/console.log "stop"))
init是在shadow-cljs.edn中指定:
:modules {:app {:init-fn example.app/init}}
^:dev/after-load 加在 start函数上, ^:dev/before-load 加在 stop函数上都是为来配合代码重新加载的生命周期.
代码刷新
我们修改了样式, 或者View层的代码后, 希望能够在保存状态的同时, 看到修改的结果. 鉴于我们是React, 我们可以很方便的挂在到shadow的生命周期上:
(def dom-root (js/document.getElementById "app"))
(defn ui []
[:div "hello world"])
(defn ^:dev/after-load start []
(reagent/render [ui] dom-root))
重新编译的触发机制
修改的命名空间会重新编译, 直接依赖它的文件 (有require关系的)会重新编译, 间接依赖的则不会, 这是对编译速度的一个妥协, 通常这种程度的编译已经足够.
+--------------------+ +---------------------+ +-------------------+
| | | (ns b | | (ns c |
| a.cljs +---------> (:require [a])) +-------> (:require [b])) |
| | | | | |
+---------+----------+ +-----------+---------+ +---------+---------+
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | \ / |
| | \- -/ v
compile----------------------->compile----------\-/-------NOT compile
-/\-
-/ \
/
注意: 如果不直接去require, shadow-cljs是不会识别这种依赖关系的.
比如说, 下面的代码在example/foo.cljs 修改的时候, 是不会触发编译的. 我们需要养成明确写明依赖的好习惯.
(ns example.foo)
(defn foo? [x]
(some.thing/else? x "foo"))
repl
比起使用生命周期的刷新, repl动态载入函数要轻量很多. repl可以以表达式 (s-expression)的粒度完成加载. 比起shadow-cljs通过watch文件的修改, 整体编译替换整个namespce的方式, repl要迅捷很多.