随着Clojure1.9的发布,给我们带来了spec和命令行的支持. mac下通过HomeBrew可以快速安装最新版clojure,自带clj命令行工具

1
brew install clojure

linux平台按照

1
2
3
curl -O https://download.clojure.org/install/linux-install.sh
chmod +x linux-install.sh
sudo ./linux-install.sh

然后通过clj命令就可以快速启动clj的repl. 原来用lein replboot repl启动需要1分钟,clj命令启动只需要短短的几秒。

1. 命令行工具clj简介

clojure命令行工具可以用来:

  • 启动REPL(Read-Eval-Print-Loop)
  • 运行clj程序
  • 求值clj表达式

当然,我们可以在clj中使用JVM平台丰富的优秀三方库 启动clj的时候,clj会从安装目录(/usr/local/Cellar/clojure/1.9.0.273/deps.edn),用户配置(~/.clojure/deps.edn),工程目录(./deps.edn)依次读取deps.edn文件合并,该文件可以:

  • 指定从本地,github工程,maven仓库或者clojar等地方拉取jar包依赖。
  • 指定source目录,默认是src(安装目录的deps.edn指定的)
  • 指定alias,可以通过-R参数来指定加载某个alias的配置
  • 指定仓库位置,默认的:mvn/repos是maven中央仓库和clojar

使用clj -Spath可以得到计算出的classpath信息,clj -Sverbose可以输出重要的路径信息,clj -Spom可以根据当前工程生成pom.xml文件

一个deps.edn示例:

1
2
3
4
5
6
{:deps
 ;; maven依赖
 {sparkledriver {:mvn/version "0.2.2"}}
 ;; 本地依赖
 {mylib {:local/root "指向mylib的路径"}}
 }

启动repl之后就能自动将maven仓库里的sparkledriver库装载到classpath以供使用。

如果要书写clj程序,默认clj会从当前目录下的src读取源文件,如果源文件写了main函数,那么 可以用clj -m 命名空间来运行,比如我有这个结构的工程,创建了src/pdf.clj

1
2
3
(ns pdf)
(defn -main []
  (println "Hello World!"))

运行clj -m pdf

one more thing

clj支持shebang了,可以作为脚本直接运行。。。

1
2
#!/usr/local/bin/clojure
(println "Hello Clojure 1.9!Shebang!")

2. 使用tools.nrepl

tools.nrepl是clj提供的远程repl服务工具,可以启动一个远程的repl服务端,也可以用来连接 另一个使用tools.nrepl启动的repl服务端.这样就能够远程求值,动态替换函数实现,实时查看远程 运行时讯息等等。我们常用来做IDE的补全和求值。

在deps.edn里加上一行org.clojure/tools.nrepl {:mvn/version "0.2.13"},然后运行clj打开repl

user=> (require '[clojure.tools.nrepl.server :as s])
nil
user=> (s/start-server :port 55555)
#clojure.tools.nrepl.server.Server{:server-socket #object[java.net.ServerSocket 0x749f539e "ServerSocket[addr=/0:0:0:0:0:0:0:0,localport=55555]"], :port 55555, :open-transports #object[clojure.lang.Atom 0x33abde31 {:status :ready, :val #{}}], :transport #object[clojure.tools.nrepl.transport$bencode 0x6a969fb8 "clojure.tools.nrepl.transport$bencode@6a969fb8"], :greeting nil, :handler #object[clojure.tools.nrepl.middleware$wrap_conj_descriptor$fn__577 0x3028e50e "clojure.tools.nrepl.middleware$wrap_conj_descriptor$fn__577@3028e50e"], :ss #object[java.net.ServerSocket 0x749f539e "ServerSocket[addr=/0:0:0:0:0:0:0:0,localport=55555]"]}

这样,nrepl server就启动在55555端口了

3. 集成cider

光有nrepl其实还没啥用,我们IDE常用的code reference,jump to definition等功能,nrepl都没有,但是cider有。cider提供的nrepl支持clj的debug,更好的错误提示,reload,变量信息,补全等功能。

在deps.edn去掉tools.nrepl,加上cider/cider-nrepl,完整示例:

1
2
{:deps
  cider/cider-nrepl {:mvn/version "0.16.0-SNAPSHOT"}}}

放在~/.clojure/deps.edn就可以不用在每个工程都指定了,然后启动server(点击可放大):

  1. (require '[clojure.tools.nrepl.server :as nrepl-server])
  2. (require '[cider.nrepl :refer (cider-nrepl-handler)])
  3. (nrepl-server/start-server :port 7888 :handler cider-nrepl-handler)

如果你用的emacs,那么可以直接cider-connect链接上去high了。

4. 完整配置

我的~/.clojure/deps.edn

1
2
3
4
5
6
7
8
9
{
  ;; 指定源码目录
  :paths ["src/main/clj" "src/main/java"]
  ;; 指定当前用户所有项目都有的依赖
  :deps {
     cider/cider-nrepl {:mvn/version "0.17.0-SNAPSHOT"}
     refactor-nrepl {:mvn/version "2.4.0-SNAPSHOT"}
  }
}

我的~/.clojure/init.clj

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
(require '[clojure.tools.nrepl.server :as nrepl-server])
(require '[cider.nrepl :refer (cider-middleware)])
(require '[refactor-nrepl.middleware :refer [wrap-refactor]])

(defn rand-range
  "返回指定区间的随机整数"
  [min max]
  (-> (rand-int max)
      (mod (inc (- max min)))
      (+ min)))

(defn gen-port
  "生成一个随机端口,写入.nrepl-port"
  []
  (let [port (rand-range 10000 65535)
        filename (-> (System/getProperty "user.dir")
                     (clojure.string/trim)
                     (str "/.nrepl-port"))]
    (spit filename  port)
    (println "端口文件已写入" filename)
    port))

(defn start-cider-nrepl-server
  "指定端口启动cider的nrepl server"
  [nrepl-port]
  (nrepl-server/start-server
   :port nrepl-port
   :handler (apply nrepl-server/default-handler
                   (map (fn [sym] (or (resolve sym)
                                      (throw (IllegalArgumentException.
                                              (format "Cannot resolve %s" sym)))))
                        (conj cider-middleware 'refactor-nrepl.middleware/wrap-refactor)))))

;; 随机端口启动cider的nrepl-server
(def cider-repl-server
  (let [need-re-start (atom true)]
    (while @need-re-start
      (let [port (gen-port)]
        (try (let [server (start-cider-nrepl-server port)]
               (reset! need-re-start false)
               (println "cider nrepl server started at" port)
               server)
             (catch Exception e))))))

启动命令使用clj -i ~/.clojure/init.clj -r,读者可以自行alias,笔者的14版最低配mac启动只花了1秒。

最后奉上两个实用的elisp函数,用于修复projectile和cider正确识别含有deps.edn的工程,放在init.el里就行。前提是安装了cider和projectile.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
  (defun sound2gd/fix-clj-edn-deps ()
    "function for working properly with clj command line tool, make cider and projectile recongnize a project with deps.edn correctly"
    (interactive)
    (projectile-register-project-type
     'clojure '("deps.edn")
     :compile "clj"
     :run "clj -i ~/.clojure/init.clj -r")
    (when (not (some (lambda (ele) (equal ele "deps.edn"))
                     clojure-build-tool-files))
      (add-to-list 'clojure-build-tool-files "deps.edn")))

  (defun sound2gd/start-depns-based-repl-server ()
    "starts a nrepl server based on clj commandline tool"
    (interactive)
    (when (file-exists-p
           (concat (file-name-directory buffer-file-name)
                   "deps.edn"))
      ;; 项目路径下deps.edn文件存在的时候才启动nrepl-server
      (call-process-shell-command "/usr/local/bin/clj -i ~/.clojure/init.clj -r")))
  ;; 17-01-02更新,在加载cider-mode的时候自动修复对deps的识别
  (add-hook 'cider-mode-hook 'sound2gd/fix-clj-edn-deps)