ClojureScript

相依性

本指南需要 ClojureScript 1.10.238 或更高版本,並假設您已熟悉快速入門.

每個非簡單的 ClojureScript 應用程式最終都需要取用其他人建立的程式碼。ClojureScript 開發人員當然可以利用使用 ClojureScript 編寫的程式碼。然而,ClojureScript 開發人員也可以取用任意 JavaScript 程式碼,無論它是否是以 ClojureScript 為前提編寫的。

本指南假設您已完成快速入門指南,並具備其中介紹的相依性。

取用 JavaScript 程式碼

雖然您可以取用任何 JavaScript 程式碼,但包含該程式碼的最佳機制並不總是相同的。以下章節將探討利用第三方 JavaScript 程式碼的各種選項。

Closure Library

最容易取用的 JavaScript 程式碼是 Google 的 Closure Library (GCL),它會自動與 ClojureScript 捆綁在一起。GCL 是一個龐大的 JavaScript 程式碼集合,組織成命名空間,很像 ClojureScript 程式碼本身。因此,您可以像 ClojureScript 命名空間一樣,從 GCL 中要求命名空間。以下範例示範基本用法

(ns hello-world.core
  (:require [goog.dom :as dom]
            [goog.dom.classes :as classes]
            [goog.events :as events])
  (:import [goog Timer]))

(let [element (dom/createDom "div" "some-class" "Hello, World!")]
  (classes/enable element "another-class" true)
  (-> (dom/getDocument)
    .-body
    (dom/appendChild element))
  (doto (Timer. 1000)
    (events/listen "tick" #(.warn js/console "still here!"))
    (.start)))

請參閱 這篇網誌文章,了解 Closure 函式庫的 :import:require 之間的差異。

它的要點是:針對 Closure 類別和列舉使用 :import,針對其他所有內容使用 :require

外部 JavaScript 函式庫

如果 GCL 沒有包含您想要的功能,或者您想利用第三方 JavaScript 函式庫,您可以直接使用程式碼。

讓我們考慮一下我們要使用一個名為 yayQuery 的精美 JavaScript 函式庫的情況。要使用 JavaScript 函式庫,只需像往常一樣參考 JavaScript。檔案是以外部方式還是內嵌方式載入都無關緊要,兩者都會在執行階段以相同的方式套用。為了簡化起見,我們將內嵌定義此函式庫

<script type="text/javascript">
    yayQuery = function() {
        var yay = {};
        yay.sayHello = function(message) {
            console.log(message);
        }
        yay.getMessage = function() {
            return 'Hello, world!';
        }
       return yay;
    };
</script>

若要從 ClojureScript 使用此函式庫,我們可以簡單地直接參考符號。如果您使用 {:optimizations :none} 建置以下程式碼,一切都將正常運作,您將在 JavaScript 主控台中看到訊息。

(ns hello-world.core)

(let [yay (js/yayQuery)]
  (.sayHello yay (.getMessage yay)))

雖然這在未最佳化的程式碼中運作良好,但當我們使用進階最佳化時,它將會失敗。嘗試使用 {:optimizations :advanced} 編譯相同的程式碼,然後重新載入您的瀏覽器。您將收到類似以下的錯誤訊息(可能與下方的不完全相同)

Uncaught TypeError: sa.B is not a function

為什麼會發生這種情況?當使用進階最佳化時,Google Closure Compiler 會重新命名符號。在大多數情況下,這不是問題,因為同一符號的所有實例都會一致地重新命名。然而,在這種情況下,外部符號(JavaScript 程式碼中的名稱)與我們的編譯單元分離,因此名稱不再匹配。幸運的是,我們有一些選項可以在不失去進階編譯的所有優點的情況下解決此問題。

使用 Externs

若要修正編譯而無需修改您的原始程式碼,您可以新增 externs 檔案。externs 檔案定義給定函式庫中的符號名稱,並由 Google Closure Compiler 用來判斷哪些符號不得重新命名。以下是我們的 yayQuery 函式庫的最小 externs 檔案

var yayQuery = function() {}
yayQuery.sayHello = function(message) {}
yayQuery.getMessage = function() {}

假設此檔案命名為 yayquery-externs.js,您可以依照以下方式在您的 build.edn 檔案中參考它

{:output-to "out/main.js"
 :externs ["yayquery-externs.js"]
 :optimizations :advanced})

務必了解 :externs 向量中參考的所有路徑都必須在類別路徑上。例如,您可能已將上述 externs 檔案放在 resources 目錄下。然後,當使用獨立的 ClojureScript JAR 時,您必須使用以下命令啟動您的建置指令碼

clj -M -m cljs.main -co build.edn -c

使用參考的 externs 檔案重新編譯,您的程式碼應該可以再次運作,而無需進行任何修改。請注意,對於許多常見的 JavaScript 函式庫,您可能會找到由函式庫作者或更廣泛的社群建立的 externs 檔案。這些檔案對於任何利用 Google Closure Compiler 的開發人員都很有用,即使是那些不使用 ClojureScript 的人也是如此。

使用字串名稱

對於您只參考少量 JavaScript 符號的簡單情況,您也可以變更您的原始程式碼,以字串名稱參考程式碼。Google Closure Compiler 永遠不會重新命名字串,因此這種樣式在不需要建立 externs 檔案的情況下也有效。即使沒有 externs,以下程式碼也會在進階編譯模式下運作

(let [yay ((goog.object.get js/window "yayQuery"))]
  ((goog.object.get yay "sayHello") ((goog.object.get yay "getMessage"))))

細心的讀者可能會注意到,我們在上面參考 js/window 的方式就像在失敗範例中參考 js/yayQuery 一樣。它在這種情況下有效,是因為 Google Closure Compiler 會隨附一些用於瀏覽器 API 的 externs。這些預設為啟用。

捆綁 JavaScript 程式碼

為了最大化內容傳遞的效率,您可以將 JavaScript 程式碼與您編譯的 ClojureScript 程式碼捆綁在一起。

與 Google Closure Compiler 相容的程式碼

如果您的外部 JavaScript 程式碼已編寫成與 Google Closure Compiler 相容,並使用 goog.provide 公開其命名空間,則包含它的最有效方式是使用 :libs 捆綁它。此捆綁機制充分利用了進階模式編譯、重新命名外部 JavaScript 函式庫中的符號並消除無效程式碼。讓我們根據以下內容調整先前範例中的 yayQuery 函式庫

goog.provide('yq');

yq.debugMessage = 'Dead Code';

yq.yayQuery = function() {
    var yay = {};
    yay.sayHello = function(message) {
        console.log(message);
    };
    yay.getMessage = function() {
        return 'Hello, world!';
    };
    return yay;
};

此程式碼與先前的內嵌版本大致相同,但現在已封裝在使用 goog.provide 公開的「命名空間」中。該函式庫可以在 ClojureScript 中輕鬆參考

(ns hello-world.core
  (:require [yq]))

(let [yay (yq/yayQuery)]
  (.sayHello yay (.getMessage yay)))

若要建置捆綁的輸出,請使用以下命令

clj -M -m cljs.main -co build.edn -O advanced -c

因為此程式碼與進階編譯相容,因此無需建立 externs。如果您查看編譯的輸出,您會看到函式已重新命名,而未參考的 debugMessage 已由 Google Closure Compiler 完全消除。

雖然這是一種非常有效率的捆綁外部 JavaScript 的方式,但大多數常見的函式庫都不與此方法相容。

捆綁「外部」JavaScript 程式碼

如果想要捆綁的程式碼在編寫時沒有考慮到 Google Closure Compiler 的相容性,您可以將其包含為外部函式庫。外部函式庫會包含在您的最終輸出中,但不會經過進階編譯。讓我們考慮一個不包含 goog.provide 的 yayQuery 版本

yayQuery = function() {
    var yay = {};
    yay.sayHello = function(message) {
        console.log(message);
    };
    yay.getMessage = function() {
        return 'Hello, world!';
    };
    return yay;
};

從 ClojureScript 中使用外部函式庫中的程式碼,與使用透過 <script> 標籤直接包含在頁面中的程式碼非常相似,但有一個關鍵差異

(ns hello-world.core
  (:require [yq]))

(let [yay (js/yayQuery)]
  (.sayHello yay (.getMessage yay)))

請注意 ns 宣告中存在 :require。這會參考名為 yq 的「命名空間」,但 yayQuery 檔案中沒有對應的 goog.provide。在外部函式庫的情況下,「命名空間」會在建置組態中提供。只要 :provides 鍵中的名稱與您 :require 的名稱相符,並且在參考的函式庫中是唯一的,您就可以隨意命名它

{:output-to "out/main.js"
 :externs ["yayquery-externs.js"]
 :foreign-libs [{:file "yayquery.js"
                 :provides ["yq"]}]}

請注意,我們在這裡重新導入了我們的 externs 檔案。雖然已捆綁外部函式庫,但必須以與外部包含指令碼的方式完全相同的方式參考它。

CLJSJS

先前的章節討論了與任何外部 JavaScript 程式碼整合的各種方式。尋找整合函式庫的最佳方式可能很棘手,特別是如果您必須取得 externs。幸運的是,對於許多最常見的 JavaScript 函式庫,有一種更簡單的方法。CLJSJS 專案會自動以 ClojureScript 編譯器直接支援的方式封裝外部 JavaScript 函式庫。它會在給定的內容中自動封裝函式庫的最佳版本(例如,在使用進階最佳化時包含縮小的函式庫),並自動包含適當的 externs。

假設我們已經超越了我們心愛的 yayQuery 函式庫,並且想要改用 jQuery。這是已預先封裝的許多熱門函式庫之一。我們可以如下擷取副本

curl -O https://clojars.org/repo/cljsjs/jquery/1.9.0-0/jquery-1.9.0-0.jar

如果您查看下載的 JAR 檔案內部 (unzip jquery-1.9.0-0.jar deps.cljs),您會看到捆綁的 deps.cljs 檔案的內容

{:foreign-libs
 [{:file "cljsjs/development/jquery.inc.js",
   :file-min "cljsjs/production/jquery.min.inc.js",
   :provides ["cljsjs.jquery"]}],
 :externs ["cljsjs/common/jquery.ext.js"]}

如果您跟著先前的章節操作,此時應該很清楚。:provides 資料會告訴我們參考此程式碼所需的一切

(ns hello-world.core
  (:require [cljsjs.jquery]))

(.text (js/$ "body") "Hello, World!")

在這種情況下,建置檔案非常簡單,因為函式庫參考完全包含在我們在叫用指令碼時會參考的 JAR 中

{:output-to "out/main.js"}

如下編譯程式碼(請注意類別路徑中新增了 JAR),當您載入瀏覽器時,應該會看到訊息顯示

clj -M -m cljs.main -co build.edn -O advanced -c

使用函式庫的另一個建置取代 (遞移) CLJSJS 相依性

有時,您在 CLJSJS 函式庫上具有遞移相依性,但想要手動包含相依性或使用它的自訂建置。在這種情況下,您需要執行兩件事:(1) 使用 :exclusions 排除相依性,以及 (2) 使用 cljsjs 名稱建立一個空的命名空間,以便建置不會中斷。

例如,om 相依於 cljsjs/react。若要包含自訂建置,您需要

;; project.cljs
;; ...
:dependencies [[org.omcljs/om "0.9.0" :exclusions [cljsjs/react]] ;; ...
;; src/cljsjs/react.cljs
(ns cljsjs.react)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.13.3/react.js"></script>
<script src="resources/public/js/compiled/your_cljs_code.js" type="text/javascript"></script>

取用 ClojureScript 程式碼

取用任何 JavaScript 函式庫的能力使 ClojureScript 成為編寫 JavaScript 應用程式的非常靈活且功能強大的語言。當然,ClojureScript 開發人員也可以輕鬆包含其他人編寫的 ClojureScript 函式庫。

直接使用函式庫

讓我們使用 Schema,這是一個 ClojureScript 函式庫,可讓我們驗證複雜的資料類型。首先,我們需要取得函式庫的副本

curl -O https://clojars.org/repo/prismatic/schema/0.4.0/schema-0.4.0.jar

與 CLJSJS 函式庫一樣,所有內容都封裝在 JAR 檔案中,我們會在編譯時在類別路徑中參考它。然而,與 CLJSJS 函式庫不同,ClojureScript 函式庫 JAR 不包含 externs 或 deps.cljs 對應。

使用該函式庫很簡單。請注意,ClojureScript 程式碼和 Clojure 巨集都封裝在同一個函式庫中

(ns hello-world.core
  (:require [schema.core :as s :include-macros true]))

(def Data {:a {:b s/Str :c s/Int}})

(s/validate Data {:a {:b "Hello" :c "World"}})

我們的建置指令碼甚至更簡單

{:output-to "out/main.js"}

現在,我們可以執行建置。只需如下參考 JAR 即可

clj -M -m cljs.main -co build.edn -c

載入您的瀏覽器,您會在 JavaScript 主控台中看到來自 Schema 的實用驗證錯誤。如果想讓這個錯誤消失,請將 :c 鍵變更為整數值,然後重新建置。