Clojure China

shadow-cljs 2.x 使用教程

#1

原文发布在 https://segmentfault.com/a/1190000011499210

以前 ClojureScript 的编译工具主要是 lein-cljsbuild 以及 boot-cljs,
现在新的工具 Lumo 和 shadow-cljs 也可以编译 ClojureScript 了.
特别是 shadow-cljs, 提供了较为完善的功能, 覆盖多方面的开发细节,
同时对于前端开发者来说, 上手也非常简单, 因为不需要处理多少 JVM 的东西.
就安装来说, 只要有 Node.js 就能一个命令安装好了:

npm install -g shadow-cljs

简单说, shadow-cljs 就是个前端开发者更容易用的 ClojureScript 编译器.
比如你有一份 ClojureScript 代码, 命名空间叫 app, 对应目录:

$ tree src/
src/
└── app
    ├── lib.cljs
    └── main.cljs

如何编译

就像 Webpack 一样, 编译之前是需要有一些配置的, 源码在哪里, 编译到哪里, 之类的,
由于 ClojureScript 有着自己的以来管理工具, 所以依赖也要卸载这个问题里:

{:source-paths ["src/"]
 :dependencies []
 :builds {:app {:output-dir "target/"
                :asset-path "."
                :target :browser
                :modules {:main {:entries [app.main]}}
                :devtools {:after-load app.main/reload!}}}}

大致解释一下关键的几个参数:

  • :target 表示编译目标, 这里选择 :browser, 有些代码只针对浏览器生成
  • :devtools 表示开发环境的配置, 这里我只配置了热替换之后执行的函数 app.main/reload!
  • :asset-path 资源存储的路径, 相对于 target/, 也影响到网页上引用代码. 虽然也可以不配置.

然后可以用 shadow-cljs 命令行来编译代码, 常用的有:

shadow-cljs compile app # 直接编译
shadow-cljs release app # 编译, 加上代码优化, 比如混淆/清除 dead code

其中的 app 就是配置里的 :app, 也叫 build id. 也就是说会有多个 build id 可以配置.

对于开发过程来说, 还有一个 watch 命令非常重要, 他就像 Webpack 一样作热替换,
因为 ClojureScript 比起 js 来说函数副作用少太多了, 所以更便于实现热替换,
搭配上边的配置, 每次文件更新, 替换浏览器运行时的代码, 然后会触发 app.main/reload! 函数:

shadow-cljs watch app

ClojureScript 有一些基础的静态检查功能, 相当于加强的 lint 工具,
所以编译当中会对有问题的代码打印警告, 甚至用 <div/> 显示在浏览器当中.

然后命令行工具还提供了其他一些开发当中用到的功能:

shadow-cljs cljs-repl app # 有 watch 服务的情况下, 再启动一个连接到浏览器的 REPL
shadow-cljs check app # 进行 release 之前可以做一些检查
shadow-cljs release app --debug # 生成 release 的代码, 同时生成 SourceMaps 等用于调试

编译目标

shadow-cljs 支持多个编译目标, 也就是对应 :target 的配置, 一般有:

  • :browser 浏览器使用的代码
  • :nodejs Node.js 开发
  • :node-library 可以被 Node.js 调用的模块
  • :npm-module 遵循 CommonJS 语法的一个个独立的 js 文件

我使用最多是 :browser, 因为功能完善, 已经能够胜任一网页应用的开发需求了,
而且 :browser 模式的打包也逐渐成熟了, 补上了一些 Webpack 有的常用功能.
在某些非常需要 Webpack 的情况下, :npm-module 可以作为一种兼容模式,
:npm-module 模式编译的代码作为 CommonJS 可以被 Webpack 直接使用.

:nodejs 模式就可以开发 Node.js 脚本, 这里热替换也是很容易就配置好的.
至于 :node-library 我还没用过, 参考文档主要是为暴露代码给 Node 调用.

关于这些模式具体的用途, 我写过一些例子, 需要时可以作为参考:

配置项

除了上面的例子, shadow-cljs 的配置项还有不少, 我拿自己的脚手架配置作为例子:

{:source-paths ["src"]
 :dependencies [[mvc-works/hsl          "0.1.2"]
                [mvc-works/shell-page   "0.1.3"]
                [mvc-works/verbosely    "0.1.0-rc"]
                [respo/ui               "0.1.9"]
                [respo/reel             "0.2.0-alpha3"]
                [respo                  "0.6.4"]]
 :http {:host "localhost" :port 8081}
 :open-file-command ["subl" ["%s:%s:%s" :file :line :column]]
 :builds {:browser {:target :browser
                    :output-dir "target/browser"
                    :asset-path "/browser"
                    :modules {:main {:entries [app.main]
                                     :depends-on #{:lib}}
                              :lib {:entries [respo.core respo.macros
                                              respo.comp.inspect]}}
                    :devtools {:after-load app.main/reload!
                               :preloads [shadow.cljs.devtools.client.hud]}
                    :release {:output-dir "dist/"
                              :module-hash-names true
                              :build-options {:manifest-name "cljs-manifest.json"}}}
          :ssr {:target :node-script
                :output-to "target/ssr.js"
                :main app.render/main!
                :devtools {:after-load app.render/main!}}}}

其中出现了些前面没有有道的配置, 我拎出来解释一下:

shadow-cljs 支持从另一个设备的浏览器上访问本机不行调试,
但是需要制定 IP 地址, 目前需要手动配置:

 :http {:host "localhost" :port 8081}

watch 模式当中浏览器上会有 div 显示 warning 的信息,
这行代码的意思是点击其中的文件链接时, 用 Sublime Text 打开, 精确到行列:

 :open-file-command ["subl" ["%s:%s:%s" :file :line :column]]

前端单页面应用倾向于生成代码到 vendor.jsmain.js 两个文件, 以往不好办,
shadow-cljs 支持将生成代码拆分为多个文件, 这里就拆分成了 main.jslib.js,
并且, 其中指定了 mainlib 的依赖, 以及 lib 包含哪些命名空间:

                    :modules {:main {:entries [app.main]
                                     :depends-on #{:lib}}
                              :lib {:entries [respo.core respo.macros
                                              respo.comp.inspect]}}

开发环境的配置, 除了常用的 :after-load, preloads 可以用来在开发时插入某些文件:

                    :devtools {:after-load app.main/reload!
                               :preloads [shadow.cljs.devtools.client.hud]}

注意 :release 的配置是写在 :browser 配置内部的, 表示覆盖重复的配置,
比如说 :output-dir "dist/" 就覆盖了外面的配置 :output-dir "target".
然后 :module-hash-names 声明对生成的文件名加上 MD5 方便放 CDN.
最后一行的配置是重命名 manifest.json 文件, 其中包含前面生成的带 MD5 的文件名:

                    :release {:output-dir "dist/"
                              :module-hash-names true
                              :build-options {:manifest-name "cljs-manifest.json"}}}

可以看很多随着 Webpack 而在前端广泛使用的功能, 在 shadow-cljs 当中做了初步的支持.

npm 模块

shadow-cljs 2.x 版本带来了在 :browser 编译目标的 npm 模块的支持, 注意写法:

(ns app.main
  (:require ["hsl" :as hsl]))

(hsl 200 80 80)

早先没有处理时的, 在 :nodejs 编译目标或者 :npm-module 当中可以这样写:

(def hsl (js/require "hsl"))

(hsl 200 80 80)

因为 require 在 Node 当中直接是函数, 在前端也可以被 Webpack 进一步处理.
随着 shadow-cljs 做了支持, 主要是针对浏览器使用做了一次大的改进,
意味着未来更多的 npm 模块可以比较方便地用到的 ClojureScript 项目当中.

除此之外, 官方的 ClojureScript 编译器也在改进 npm 支持, 不清楚进展.

小结

目前文档还在计划当中, 部分功能需要在 Wiki 当中查找细节,



还有一些问题是我们已经躺过的, 所以在 Issues 里有记录, 值得搜索:

反正呢, 就是比以前好用很多了, 对于打包来说更是好了很多…
至于遇到什么 bug, 直接去 Issues 列表上问作者吧, 我已经 po 了好多 issue 了.
而且 shadow-cljs 相对来说比较年轻, 遇到 bug 也正常, 还好作者修得很快, 时间多…
Slack 上直接联系作者也可以, 英语够好的话. 另外作者在欧洲, 注意时差:
https://clojurians.slack.com/messages/shadow-cljs

1赞