Clojure China

介绍一下 shadow-cljs 的使用和体验

#1

仓库 https://github.com/thheller/shadow-cljs
npm https://www.npmjs.com/package/shadow-cljs

shadow-cljs 是一个 ClojureScript 编译器的封装,
和 lein-cljsbuild + lein-figwheel 或者 boot-cljs + lein-reload 功能类似.
它能够提供对于浏览器平台和 Node.js 平台代码编译, 优化, 热替换.
这个项目是最近才冒出来的, 近期做了不少的修改, 名字也定下来不久.

作者是 Thomas Heller https://twitter.com/thheller 在 Clojurians 群里好像一直很活跃,
作者经常说: 我在等你们试用然后提 issue…

优点

shadow-cljs 相对于以往的方案有两个优点, 主要针对 JavaScript 开发者:

  • 通过 npm 安装
npm i shadow-cljs

系统里安装了 npm 的同学都可能很快安装上 shadow-cljs,
我建议在项目根路径安装, 然后通过 npm script 调用, 目前这是 js 社区常用的玩法.
JVM 被工具内部管理了, 不需要复杂的配置, 尽管功能会局限一点.
如果系统已经安装 JVM 会直接用, 没有的话估计要全局装一下 node-jre 模块.

  • 生成 CommonJS 代码

跟以往的编译器不一样的是, shadow-cljs 支持生成 CommonJS 格式的代码,
我们知道 ClojureScript 用了 Closure Compiler 的命名空间的, 跟 Node 不兼容,
而 shadow-cljs 做的是强行在代码当中插入了 CommonJS 的 require/exports 代码,
总体上已经能做到正常地放进 Webpack 打包, 以及用 node 命令直接运行编译结果.

用法

前几周更新比较频繁, 可能后面细节会调整, 这里基于 0.9.4 介绍.

  • 编译到 CommonJS

shadow-cljs 的配置文件是 EDN 文件, 大致结构是:

{:dependencies []
 :source-paths ["src"]
 :builds {:app {:target :npm-module
                :output-dir "compiled/"}}}

这里定义了一个编译策略叫做 app, 命令行当中就可以使用这个配置:

shadow-cljs --build app --once

这个命令就会把 src/ 目录下的 cljs 文件统一编译到 compiled/ 目录下面.
其中 :npm-module 这个选项设定了编译结果是 npm 模块用的文件.
除了 --once, 还有两个常用的命令:

shadow-cljs --build app --dev # 监视文件修改自动编译
shadow-cljs --build app --release # 使用 :advanced 模式编译优化代码

--dev 模式下, 文件类似增量编译, 可以被 Webpack 监听到,
通过这个方式, CommonJS 文件可以和 Webpack 比较好地配合使用.

可以翻阅我写的例子 https://github.com/minimal-xyz/minimal-shadow-cljs-commonjs
配合 Webpack 热替换的例子 https://github.com/minimal-xyz/minimal-shadow-cljs-webpack
也可以看 Wiki https://github.com/thheller/shadow-cljs/wiki/ClojureScript-for-JS-Devs

  • 编译到浏览器

如果是编译到浏览器, 作者提供的 demo 是这样的:

{:dependencies []
 :source-paths ["src"]
 :builds {:app {:target :browser
                :output-dir "public/js"
                :asset-path "/js"
                :modules {:main [my.app]}}}}

如果要开启代码热替换, 需要配置 :devtools 选项激活热替换的代码:

{:target :browser
 ; ...
 :devtools {:before-load my.app/stop
            :after-load my.app/start}}

激活热替换之后, 还可以启动 REPL 用来发送 cljs 到浏览器执行.

:modules 配置用来指定程序启动的入口的文件, 前面 npm 模块里用不到这个,
其他还提供了一些相对高级的策略比如分包, 用来处理处理浏览器环境的细节,
具体看 Wiki https://github.com/thheller/shadow-cljs/wiki/ClojureScript-for-the-browser
不过我倾向还是用 Webpack 来处理奇怪的场景, 毕竟 js 这边套路比较多.

用 Webpack 的时候会注意到 SourceMaps 并不是立即生效的,
需要在 Webpack 配置里读取已有的信息, 继续生成 SourceMaps,
虽然体验不是完美, 但是对于调试多多少少还是有帮助的:

devtool: 'source-map',
      {
        test: /\.js$/,
        loader: 'source-map-loader',
        options: { enforce: 'pre' }
  • 编译到 Node.js 代码

大体上跟浏览器的配置很像, 但是提供了两个 :target 配置 :node-library:node-script.
:node-scriptnpm-module 的用法很像, 我没用过, 估计是对 Node.js API 做了处理,
跟浏览器配置差不多, 也支持热替换, 可以看我写的例子:


关于编译的细节在 Wiki 上也有一些描述, 自己看啦, 估计作者文档没写完…

:advanced 模式

这个是 Closure Compiler 提供的编译选项, 用于代码优化.
以往的 ClojureScript 编译器也是支持的, 只是在这里针对 CommonJS 特殊处理了一下.
技术细节可以阅读 https://github.com/thheller/shadow-cljs/issues/24

ClojureScript 代码编译结果比较大, 需要剔除无用的代码, 要开启 :advanced 模式
但是这是需要写 externs 文件指明如何保留不删除和混淆的代码的…
对于大多数人来说编写 externs 是一件很头疼的事情, 所以还是要想点办法,
目前作者也讲还在考虑当中, 看如何才能把这个过程简化一下.

对比

大致使用体验的话, 可能启动会快一点, 毕竟不像 Boot 那么多初始化的东西,
不过 shadow-cljs 不提供 compile warning 在页面弹提示, 可能会不习惯,
现在应该遇到编译出错会在 Console 里打印的. 其他开发体验应该还好.
我是说直接用 :browser 模式开发, 工作流程其实差不多,

用了 Webpack 的话, 相当于是经过两个不同的工具编译, 不那么可靠,
但是引入 Webpack 主要还是为了 npm 模块还有打包之类的事情的方便吧.

Node 这边体验好一点, 以前的 lein-figwheel 虽然功能强大, 但是配置太烦了,
shadow-cljs 是开箱即用, :devtools 直接配好热替换, 直接有 REPL,
另外 Lumo 虽然也能玩 Node, 我的感觉是启动太慢了,
shadow-cljs 这边 watching compiling 开起来, 然后就是 node 启动的速度了.
而且 Lumo 的做法, 热替换需要自己处理, --inspect 选项不能用, 不够好.

其他

总体感觉是开启了很多的可能性, 特别是对 js 开发者能友好很多.
之前配置 classpath 真是搞得烦死了, 现在完全不用管.
最近 cljs 社区似乎在接入 npm 方面做了一些努力, 感觉能舒心一点.

现在看看开发当中, 还有往 Clojars 上传模块这个过程离不开 Boot 或者 Lein,
不知道近期有没有新的方案出来, 那样的话我手头的 build.boot 就能删了.
有点盼头总是好的…

2赞
#2

:clap::clap::clap:

#3

Js真的成了 “The Modern C Language”, 浏览器平台的胶水语言

#4

:heart_eyes: