[譯] ClojureScript 中的 JavaScript 互操做

機翻爲主, 原文: ClojureScript: JavaScript Interop
http://www.spacjer.com/blog/2014/09/12/clojurescript-javascript-interop/javascript

(原文更新於 15th of March 2015)html

正如我在這個博客上提到過,我在持續不斷學習的 Clojure(和 ClojureScript)。爲了更好地理解語言,我已經寫了小型 Web 應用程序。爲了好玩,我決定,我全部的前端代碼將被寫入 ClojureScript。由於我須要使用外部JavaScript API(Bing 地圖 AJAX 控件),我寫了至關多的 JavaScript 的互操做碼 -- 對我來講語法並不明顯,我找不到有全部這些信息的地方,因此我寫了這篇文章。請注意,這是一個至關長的帖子!前端

JavaScript 例子

爲了更容易理解全部的例子能夠定義簡單的 JavaScript 代碼:java

//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 對象)。node

(def text js/globalName)

JS 輸出:git

namespace.text = globalName;

建立對象

ClojureScript 中能夠經過在構造函數的結尾添加 . 建立 JavaScript 對象:github

(def t1 (js/MyType.))

JS 輸出:api

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 的優秀文章

像往常同樣,我讚揚任何評論。

相關文章
相關標籤/搜索