ClojureScript

依赖项

本指南需要ClojureScript 1.10.238或更高版本,并假设您熟悉快速入门.

每个非平凡的ClojureScript应用程序最终都需要使用其他人编写的代码。ClojureScript开发人员当然可以利用使用ClojureScript编写的代码。但是,ClojureScript开发人员也可以使用任意JavaScript代码,无论它是使用ClojureScript编写还是没有。

本指南假设您已完成快速入门指南,并具备其中介绍的依赖项。

使用JavaScript代码

虽然您可以使用任何JavaScript代码,但包含该代码的最佳机制并不总是相同的。以下部分将探讨使用第三方JavaScript代码的各种选项。

Closure 库

最容易使用的JavaScript代码是Google的Closure 库 (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)))

请参阅这篇博文,了解:import:require用于Closure库之间的区别。

它的要点是:使用:import用于Closure类和枚举,使用: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编译器将重命名符号。在大多数情况下,这不是问题,因为同一符号的所有实例都将被一致地重命名。但是,在本例中,外部符号(JavaScript代码中的名称)与我们的编译单元分离,因此名称不再匹配。幸运的是,我们有解决此问题的方法,而不会失去高级编译的所有好处。

使用Externs

要修复编译而不修改您的源代码,您可以添加一个externs文件。Externs文件定义了给定库中的符号名称,并被Google Closure编译器用来确定哪些符号不能被重命名。以下是我们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编译器的开发人员都很有用,即使他们没有使用ClojureScript。

使用字符串名称

对于您只引用少量JavaScript符号的简单情况,您还可以更改您的源代码以通过字符串名称引用代码。Google Closure编译器永远不会重命名字符串,因此这种样式无需创建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编译器随附了许多针对浏览器API的externs文件。这些文件默认情况下已启用。

捆绑JavaScript代码

为了最大程度地提高内容交付效率,您可以将JavaScript代码与已编译的ClojureScript代码捆绑在一起。

Google Closure编译器兼容代码

如果您的外部JavaScript代码已编写为与Google Closure编译器兼容,并使用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编译器完全删除。

虽然这是一个捆绑外部JavaScript的极其有效的方式,但大多数流行的库都不兼容此方法。

捆绑“外部”JavaScript代码

如果要捆绑的代码不是使用Google Closure编译器兼容性来编写的,您可以将其作为外部库包含。外部库包含在您的最终输出中,但不会通过高级编译。让我们考虑一个不包含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项目自动将外部JavaScript库打包成ClojureScript编译器直接支持的方式。它将自动将给定上下文中的最佳版本库打包在一起(例如,在使用高级优化时包含最小化库),并自动包含相应的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 键更改为整数值并重新构建。