May 15, 2020
By: Kevin

shadow-cljs介绍

  1. 什么是shadow-cljs
  2. 配置
    1. target
  3. shadow-cljs的热更新
    1. 生命周期
    2. 代码刷新
    3. 重新编译的触发机制
  4. repl
  5. 参考资料

什么是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要迅捷很多.

参考资料

Tags: shadow-cljs clojurescript