在 ClojureScript 中使用 macro 简化 Angular.js 代码

背景

在 Angular.js 中配置 module.configmodule.controller 等时, 需要使用一种稍微麻烦一点的方式:

function PhoneListCtrl($scope, $http) {...}
phonecatApp.controller('PhoneListCtrl', ['$scope', '$http', PhoneListCtrl]);

即需要重复指定需要的参数: 函数的参数和数组中的字符串. 这样做的目的是防止这些 JavaScript 代码被压缩混淆后, 函数的参数被重命名以至于 Anuglar.js 不知道这些参数对应哪些 service (详细说明参见官方的教程).

目标

我们更想要的是只定义一次, 例如:

function PhoneListCtrl($scope, $http) {...}
phonecatApp.controller('PhoneListCtrl', PhoneListCtrl);

但可以达到在压缩时不被混淆的目的.

解决方案: ngmin

于是社区就有了一个解决方案 ngmin. ngmin 工作方式类似于 CoffeeScript 和 Sass, 即预处理我们的代码, 并把我们写的简单的定义方式替换成完整的方式. 使用 ngmin 需要 Node.js (npm).

解决方案: ClojureScript macro

如果你使用 ClojureScript, 它内置的 macro (宏) 功能可以优雅的解决这个问题. 定义一个 macro:

(defmacro expand-ng [args & body]
  (let [func `(fn [~@args] ~@body)
        args-with-func (conj (mapv name args) func)]
    `(cljs.core/array ~@args-with-func)))

这样, (expand-ng [$scope, $http]) 就会得到下面的 JavaScript:

['$scope', '$http', function($scope, $http) { }]

在此基础上定义另一个 helper macro:

(defmacro defng [name args & body]
  (let [expand `(expand-ng ~args ~@body)]
    `(def ~name ~expand)))

就可以这样使用了:

(def app
  (.module js/angular "hackApp" (array "ngRoute")))

;; 这里
(defng main-ctrl [$scope]
  (set! (.-name $scope) "Marvin"))

(-> app
    (.controller "MainCtrl" main-ctrl))

总结

ClojureScript 是 Clojure 在 JavaScript 平台上的实现. Clojure 是运行在 JVM 平台上的一种 Lisp 方言. macro 正是 Clojure 继承自 Lisp 的一个强大的功能.

通过 macro 可以在代码中定义领域相关的"内嵌语言" (DSL), 专门用于解决当前针对的问题. 使用设计良好的 DSL 可以让代码更少更可读. 而且由于 macro 在代码的编译期执行的特点, 所以 macro 不会添加额外的性能开销, 甚至可以通过在 macro 中计算某些耗时的结果从而提升性能.

P.S. 已经有3本 Clojure 经典图书出了中译本: Clojure编程, Clojure程序设计Clojure编程乐趣. 入门推荐前两本选一个 (第一本较深入和全面)

P.P.S 使用 ClojureScript 做 Angular.js 开发的同学可以关注下 clanggyr