5

机翻为主, 原文: 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 要做到这一点,可以使用 asetset! 函数:

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}}]}

正如你可以看到上面定义了两个构建:devrelease。请注意 :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 CompilerexportSymbol 函数来完成 - 但我不会详谈细节)。然后在你的外部 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 的优秀文章

像往常一样,我赞赏任何评论。


题叶
17.3k 声望2.6k 粉丝

Calcit 语言作者


引用和评论

0 条评论