4

shadow-cljs 是一个新开发的 ClojureScript 开发和编译工具.
以前编译主要是 lein-cljsbuild, boot-cljs, lein-figwheel,
现在新的工具 Lumo 和 shadow-cljs 也可以完成编译工作了.
特别是 shadow-cljs 的功能覆盖开发当中很多场景, 对 JavaScript 开发者更友好.
对于前端开发者来说, shadow-cljs 上手也非常简单, 不需要去管 JVM 的事情.
安装 shadow-cljs 非常简单, 通过 npm 的命令来就好了:

npm install -g shadow-cljs

比如你有一个 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/, 也影响到网页上引用代码. 默认似乎是 ./.

更多的参数可以到文档站点上查阅: http://doc.shadow-cljs.org/

配置完成之后可以用 shadow-cljs 命令来编译代码, 常用的子命令有:

shadow-cljs compile app # 每个文件直接编译到对应一个 js 文件
shadow-cljs release app # 编译, 进行代码合并和优化, 以及清除 dead code

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

开发过程中, 最常用的是 watch 命令, 它就像 webpack-dev-server,
ClojureScript 相比 js 来说每个函数副作用更少, 所以更适合进行热替换,
基于上边的配置, 在每次文件更新, 浏览器就会进行代码热替换, 然后会触发 app.main/reload! 函数:

shadow-cljs watch app # 启动编译器, 监视文件更新自动编译

ClojureScript 有一些基础的静态检查功能, 相当于加强的 lint 工具,
所以编译当中会检查代码代码并打印警告, 以及的浏览器当中弹出警告内容.

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

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

更多的子命令可以查阅 http://doc.shadow-cljs.org/

编译目标

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

  • :browser 运行在浏览器的代码
  • :node-script 运行在 Node.js 的代码
  • :node-library 可以被 Node.js JavaScript 代码调用的模块
  • :npm-module 遵循 CommonJS 语法的独立的 js 文件

我使用最多是 :browser, 功能完善, 已经能够胜任目前网页应用的开发需求了,
而且 :browser 模式的打包也逐渐成熟了, 补上了一些 Webpack 中的常用功能.
在某些只能通过 Webpack 打包情况下, 可以使用 :npm-module 作为一种兼容模式,
:npm-module 模式编译的代码符合 CommonJS 规范, 可以被 Webpack 用于打包(注意这样打包带上 ClojureScript 的 runtime 代码是挺大的).

:node-script 用于开发 Node.js 脚本, 这里热替换也是基本一致的配置.
至于 :node-library 我还没用过, 参考文档应该是暴露结构给 Node.js 脚本调用.

关于这些模式具体的用途, 我搜集了一些例子, 可以参考:

配置项

除了上面的例子, 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!
                               :http-root "target"
                               :http-port 7000}
                    :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 内置了一个 HTTP 服务器用于网页的调试,
需要在 :devtools 的配置当中添加 HTTP 相关的配置:

:devtools {:http-root "target"
           :http-port 7000}

watch 模式当中遇到代码存在顺发错误, 浏览器上会有界面显示 warning,
shadow-cljs 支持点击代码打开编辑器对应的行, 通过配置打开文件的命令,
传输我用 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, 还有 :before-load 等:

:devtools {:after-load app.main/reload!}

注意 :release 的配置是写在 :browser 配置内部的, 表示覆盖重复的配置,
这是 shadow-cljs 提供的一个简写, 你也可以自己专门写一遍 :release 的配置.
比如说 :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"}}}

:release 的配置可也支持别的配置, 比如这里的 8 表示 Hash 的长度,
manifest 文件除了 JSON, 也可以通过文件后缀支持生成 EDN 文件:

:release {:output-dir "dist/"
          :module-hash-names 8
          :build-options {:manifest-name "assets.edn"}}}

可以看很多随着 Webpack 而在前端广泛使用的功能, 在 shadow-cljs 当中做了不少的支持.
代码拆包以后, shadow-cljs 不好做异步加载, 这个是有些不足, 可以向官方反馈.

npm 模块

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

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

(hsl 200 80 80)

:node-script 编译目标或者 :npm-module 当中也支持这样写:

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

(hsl 200 80 80)

因为 require 在 Node 当中直接是函数, 在前端也可以被 Webpack 进一步处理.
大部分 npm 模块都可以直接用到的 ClojureScript 项目当中.
除此之外, Lumo 和官方的 ClojureScript 编译器也改善了对 npm 模块支持.

小结

shadow-cljs 的文章已经做得比较完善, 可以访问 http://doc.shadow-cljs.org 查阅.
如果遇到问题或者想要反馈, 可以通过下面两个地址提交:
https://github.com/thheller/s...
https://clojureverse.org/c/pr...
英语够好的话甚至直接到聊天室上找到作者, 作者在欧洲, 注意时差:
https://clojurians.slack.com/...
2017 年秋天至今 shadow-cljs 作者都在很积极更新功能,
很多的 bug 都以非常快的速度修复了, 让 shadow-cljs 更加友好.


题叶
17.3k 声望2.6k 粉丝

Calcit 语言作者


引用和评论

0 条评论