机翻为主, 原文: ClojureScript: JavaScript Interop
http://www.spacjer.com/blog/2014/09/12/clojurescript-javascript-interop/
(原文更新于 15th of March 2015)
正如我在这个博客上提到过,我在持续不断学习的 Clojure(和 ClojureScript)。为了更好地理解语言,我已经写了小型 Web 应用程序。为了好玩,我决定,我所有的前端代码将被写入 ClojureScript。因为我需要使用外部JavaScript API(Bing 地图 AJAX 控件),我写了相当多的 JavaScript 的互操作码 -- 对我来说语法并不明显,我找不到有所有这些信息的地方,所以我写了这篇文章。请注意,这是一个相当长的帖子!
JavaScript 例子
为了更容易理解所有的例子可以定义简单的 JavaScript 代码:
//global variable
globalName = "JavaScript Interop";
globalArray = globalArray = [1, 2, false, ["a", "b", "c"]];
globalObject = {
a: 1,
b: 2,
c: [10, 11, 12],
d: "some text"
};
//global function
window.hello = function() {
alert("hello!");
}
//global function
window.helloAgain = function(name) {
alert(name);
}
//a JS type
MyType = function() {
this.name = "MyType";
}
MyComplexType = function(name) {
this.name = name;
}
MyComplexType.prototype.hello = function() {
alert(this.name);
}
MyComplexType.prototype.helloFrom = function(userName) {
alert("Hello from " + userName);
}
全局作用域
ClojureScript 定义了特殊的 js
命名空间允许访问 JavaScript 类型/函数/方法/全局对象(即浏览器 window
对象)。
(def text js/globalName)
JS 输出:
namespace.text = globalName;
创建对象
ClojureScript 中可以通过在构造函数的结尾添加 .
创建 JavaScript 对象:
(def t1 (js/MyType.))
JS 输出:
namespace.t1 = new MyType;
(注:起初我以为,这产生的 JS 代码是因为缺少括号错了,但它实际上是有效的 - 如果构造函数没有参数,那么括号可省略)
还有创建对象的不同的方式,使用 new
函数(JS 构造函数的名称应该是没有点号):
(def my-type (new js/MyComplexType "Bob"))
JS 输出:
namespace.my_type = new MyComplexType("Bob");
调用方法
要调用 JavaScript 方法,我们需要方法名之前加上 .
(点号):
(.hello js/window)
JS 输出:
window.hello();
去掉语法糖就是:
(. js/window (hello))
将参数传递给我们的函数:
(.helloAgain js/window "John")
JS 输出:
window.helloAgain("John");
或者:
(. js/window (helloAgain "John"))
同样的事情可以通过创建对象来完成:
(def my-type (js/MyComplexType. "Bob"))
(.hello my-type)
JS 输出:
namespace.my_type = new MyComplexType("Bob");
namespace.my_type.hello();
访问属性
ClojureScript 提供一些方法 JavaScript 操作属性。最简单的一种是使用 .-
属性访问语法:
(def my-type (js/MyType.))
(def name (.-name my-type))
JS 输出:
namespace.my_type = new MyType;
namespace.name = namespace.my_type.name;
类似的事情可以通过 aget
函数,它接受对象和属性的名称(字符串)作为参数来完成:
(def name (aget my-type "name"))
JS 输出:
namespace.name = namespace.my_type["name"];
aget
也允许访问嵌套的属性:
(aget js/object "prop1" "prop2" "prop3")
JS 输出:
object["prop1"]["prop2"]["prop3"];
同样的事情(生成的代码是不同的)可以做到通过使用 ..
语法完成:
(.. js/object -prop1 -prop2 -prop3)
JS 输出:
object.prop1.prop2.prop3;
您还可以设置一个属性的值,ClojureScript 要做到这一点,可以使用 aset
或 set!
函数:
该 aset
函数将属性作为一个字符串的名字:
(def my-type (js/MyType.))
(aset my-type "name" "Bob")
JS 输出:
namespace.my_type["name"] = "Bob";
而 set!
需要一个属性访问:
(set! (.-name my-type) "Andy")
JS 输出:
namespace.my_type.name = "Andy";
Array
aget
函数也可用于访问 JavaScript 数组元素:
(aget js/globalArray 1)
JS 输出:
globalArray[1];
或者,如果你想获得嵌套的元素,您可以以这种方式使用它:
(aget js/globalArray 3 1)
JS 输出:
globalArray[3][1];
嵌套作用域
这个主题对我来说有点混乱。在我的项目,我想翻译这样的代码:
var map = new Microsoft.Maps.Map();
到 ClojureScript。正如你所看到的 Map
函数在嵌套的作用域中。访问嵌套属性的惯用方法是使用 ..
或 aget
函数,但是这不能用于构造函数来完成。在这种情况下,我们需要用点号(即使它不是地道的 Clojure 的代码):
(def m2 (js/Microsoft.Maps.Themes.BingTheme.))
或使用 new
函数:
(def m1 (new js/Microsoft.Maps.Themes.BingTheme))
如果我们这样写这个表达式:
(def m3 (new (.. js/Microsoft -Maps -Themes -BingTheme)))
我们将得到一个异常:
First arg to new must be a symbol at line
core.clj:4403 clojure.core/ex-info
analyzer.clj:268 cljs.analyzer/error
analyzer.clj:265 cljs.analyzer/error
analyzer.clj:908 cljs.analyzer/eval1316[fn]
MultiFn.java:241 clojure.lang.MultiFn.invoke
analyzer.clj:1444 cljs.analyzer/analyze-seq
analyzer.clj:1532 cljs.analyzer/analyze[fn]
analyzer.clj:1525 cljs.analyzer/analyze
analyzer.clj:609 cljs.analyzer/eval1188[fn]
analyzer.clj:608 cljs.analyzer/eval1188[fn]
MultiFn.java:241 clojure.lang.MultiFn.invoke
analyzer.clj:1444 cljs.analyzer/analyze-seq
analyzer.clj:1532 cljs.analyzer/analyze[fn]
analyzer.clj:1525 cljs.analyzer/analyze
analyzer.clj:1520 cljs.analyzer/analyze
compiler.clj:908 cljs.compiler/compile-file*
compiler.clj:1022 cljs.compiler/compile-file
创建 JavaScript 对象
有许多情况下,我们需要从 ClojureScript 的方法传递 JavaScript 对象。一般 ClojureScript 能处理自己的数据结构(不可变的,持久的 vector,Map,set 等)转化为纯的 JS 对象。有这样做的几种方法。
如果我们要键值对列表中创建一个简单的 JavaScript 对象, 我们可以用 js-obj
这个宏:
(def my-object (js-obj "a" 1 "b" true "c" nil))
JS 输出:
namespace.my_object_4 = (function (){var obj6284 = {"a":(1),"b":true,"c":null};return obj6284;
需要注意的是 js-obj
强迫你使用字符串作为键和基础数据的字面量(字符串,数字,布尔值)的值。ClojureScript 数据结构不会改变,所以这样的:
(def js-object (js-obj :a 1 :b [1 2 3] :c #{"d" true :e nil}))
会创建这样的 JavaScript 对象:
{
":c" cljs.core.PersistentHashSet,
":b" cljs.core.PersistentVector,
":a" 1
}
你可以看到有使用的内部类型,如:
cljs.core.PersistentHashSet
cljs.core.PersistentVector
ClojureScript 关键字改为字符串前面加上冒号。
为了解决这个问题,我们可以使用 clj-> js
函数:“递归转换 ClojureScript 值到 JavaScript。Set / Vector / List 成为 Array,Keyword 和 Symbol 成为字符串,Map 成为 Object“。
{
"a": 1,
"b": [1, 2, 3],
"c": [null, "d", "e", true]
}
也有生产的 JavaScript 对象的另一种方式 -- 我们可以使用 #js
reader 语法:
(def js-object #js {:a 1 :b 2})
生成的代码:
namespace.core.js_object = {"b": (2), "a": (1)};
使用 #js
时,你需要谨慎,因为这个语法也不会改变内部结构(这是浅层的):
(def js-object #js {:a 1 :b [1 2 3] :c {"d" true :e nil}})
会创建这样的对象:
{
"c": cljs.core.PersistentArrayMap,
"b": cljs.core.PersistentVector,
"a": 1
}
要解决这个问题,你需要在每个 ClojureScript 结构前添加 #js
:
(def js-object #js {:a 1 :b #js [1 2 3] :c #js ["d" true :e nil]})
JavaScript 对象:
{
"c": {
"e": null,
"d": true
},
"b": [1, 2, 3 ],
"a": 1
}
使用 JavaScript 对象
有些时候,我们需要转换的 JavaScript 对象或数组到 ClojureScript的数据结构的情况。我们可以通过使用 js->clj
函数做到这一点:
“递归转变 JavaScript 数组到 ClojureScript Vector,和 JavaScript 对象到ClojureScript Map。通过选项 :keywordize-key true
将对象字段从转换
字符串的 Keyword。
(def my-array (js->clj (.-globalArray js/window)))
(def first-item (get my-array 0)) ;; 1
(def my-obj (js->clj (.-globalObject js/window)))
(def a (get my-obj "a")) ;; 1
作为函数的文档说明的,可以使用 :keywordize-keys true
转换创建好的 Map 的关键字字符串到 keyword:
(def my-obj-2 (js->clj (.-globalObject js/window) :keywordize-keys true))
(def a-2 (:a my-obj-2)) ;; 1
此外
如果使用 JavaScript 的所有其他方法都失败,有一个 js*
接收一个字符串作为参数,并原样返回作为 JavaScript 代码:
(js* "alert('my special JS code')") ;; JS output: alert('my special JS code');
暴露 ClojureScript 函数
值得注意的是,在从 ClojureScript 生成 JavaScript 代码的确切形式取决于编译器设置。这些设置可以在 Leiningen project.clj
文件中定义:
project.clj
文件的相关部分:
:cljsbuild {
:builds [{:id "dev"
:source-paths ["src"]
:compiler {
:main your-namespace.core
:output-to "out/your-namespace.js"
:output-dir "out"
:optimizations :none
:cache-analysis true
:source-map true}}
{:id "release"
:source-paths ["src"]
:compiler {
:main blog-sc-testing.core
:output-to "out-adv/your-namespace.min.js"
:output-dir "out-adv"
:optimizations :advanced
:pretty-print false}}]}
正如你可以看到上面定义了两个构建:dev
和 release
。请注意 :optimizations
参数 -- 使用 :advanced
的代码将被压缩(未使用的代码被删除),并更名(使用较短的名称)。
例如,该 ClojureScript 代码:
(defn add-numbers [a b]
(+ a b))
在:advanced
模式将被编译到这样的 JavaScript 代码 :
function yg(a,b){return a+b}
函数名称是完全“随机”,所以你不能从 JavaScript 文件中使用它。为了能够使用ClojureScript 函数定义(其原始名称),你应该加上标志 :export
作为 metadata:
(defn ^:export add-numbers [a b]
(+ a b))
这个 :export
关键字告诉编译器给定函数名导出到外部。(这是通过 Google Closure Compiler 的 exportSymbol
函数来完成 - 但我不会详谈细节)。然后在你的外部 JavaScript 代码,你可以调用这个函数:
your_namespace.core.add_numbers(1,2);
请注意,所有的破折号,取而代之的是下划线。
使用外部 JavaScript 库
:advanced
模式也影响到外部库的调用,因为所有的函数/方法的名称更改为最小的形式。让我们来 ClojureScript 代码,从 Chart
对象调用PolarArea
函数:
(defn ^:export creat-chart []
(let [ch (js/Chart.)]
(. ch (PolarArea []))))
编译完成后,该代码将类似于这样:
function(){return(new Chart).Bc(zc)}
正如你所看到的,PolarArea
方法改为 Bc
,这当然会导致运行错误。为了防止这种情况,我们需要告诉编译器哪些名字不应该被改变。这些名称应在外部 JavaScript 文件中定义(即 externs.js
)并提供给编译器。在我们的例子中 externs.js
文件看起来应该像这样的:
var Chart = {};
Chart.PolarArea = function() {};
关于这个文件, 编译器应该通过project.clj
中的 :externs
设置被告知 :
{:id "release"
:source-paths ["src"]
:compiler {
:main blog-sc-testing.core
:output-to "out-adv/your-namespace.min.js"
:output-dir "out-adv"
:optimizations :advanced
:externs ["externs.js"]
:pretty-print false}}
如果我们做所有这些事情,创建 JavaScript 代码将包含 PolarArea
函数的正确调用:
function(){return(new Chart).PolarArea(Ec)}
要获得有关 ClojureScript 使用外部 JavaScript 库的更多详细信息,关于这一点我建议你阅读 Luke VanderHart 的优秀文章。
像往常一样,我赞赏任何评论。