-
Notifications
You must be signed in to change notification settings - Fork 790
Dependencies
Every non-trivial ClojureScript application will eventually need to consume code created by others. ClojureScript developers can of course take advantage of code authored using ClojureScript. However, ClojureScript developers can also consume arbitrary JavaScript code, whether or not it was written with ClojureScript in mind.
This guide assumes you've worked through the Quick Start guide, and are equipped with the dependencies introduced there.
While you can consume any JavaScript code, the optimal mechanism for including that code is not always the same. The following sections explore the various options for utilizing third party JavaScript code.
The easiest JavaScript code to consume is that of Google's Closure Library (GCL), which is automatically bundled with ClojureScript. GCL is a massive collection of JavaScript code organized into namespaces much like ClojureScript code itself. Thus, you can require a namespace from GCL in the same fashion as a ClojureScript namespace. The following example demonstrates basic usage:
(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))
In cases where GCL doesn't contain the functionality you want, or you'd otherwise like to take advantage of a third party JavaScript library, you can use the code directly.
Let's consider the case where we want to use a fancy JavaScript library called yayQuery. To utilize a JavaScript library, simply reference the JavaScript as normal. Whether the file is loaded externally or inline makes no difference, both will be applied in the same fashion at runtime. To make things simple, we'll define this library inline:
<script type="text/javascript">
yayQuery = function() {
var yay = {};
yay.sayHello = function(message) {
console.log(message);
}
yay.getMessage = function() {
return 'Hello, world!';
}
return yay;
};
</script>
To use this library from ClojureScript, we can simply refer to the symbols directly. If you build the following code using {:optimizations :none}
, everything will work fine and you will see a message in your JavaScript console.
(ns hello-world.core)
(let [yay (js/yayQuery)]
(.sayHello yay (.getMessage yay)))
While this works fine with unoptimized code, it will fail when we use advanced optimizations. Try compiling the same code with {:optimizations :advanced}
and reload your browser. You will receive an error message similar to the following (it may not be exactly as below):
Uncaught TypeError: sa.B is not a function
Why did this happen? When using advanced optimizations, the Google Closure Compiler will rename symbols. In most cases, this is not a problem, as all instances of the same symbol will be renamed consistently. However, in this case the external symbol (the name in the JavaScript code) is separate from our compilation unit, so the names no longer match. Fortunately, we have options for resolving this issue without losing all of the benefits of advanced compilation.
To fix compilation without modifying your source code at all, you can add an externs file. An externs file defines the symbol names in a given library, and is used by Google Closure Compiler to determine which symbols must not be renamed. Here's a minimal externs file for our yayQuery library:
var yayQuery = function() {}
yayQuery.sayHello = function(message) {}
yayQuery.getMessage = function() {}
Assuming this file is named as yayquery-externs.js
, you can reference it as follows in your build.clj
file:
(cljs.closure/build "src" {:output-to "out/main.js"
:externs ["yayquery-externs.js"]
:optimizations :advanced})
Recompile with the externs file referenced, and your code should work again without any modifications. Note that for many popular JavaScript libraries, you may be able to find externs files which have already been created by the library authors or the broader community. These files are useful for any developer taking advantage of Google Closure Compiler, even those not using ClosureScript.
For simple cases where you only reference a small number of JavaScript symbols, you can also change your source code to reference code by string name. Google Closure Compiler will never rename strings, so this style will work without needing to create an externs file. The code below will work in advanced compilation mode even without externs:
(let [yay ((aget js/window "yayQuery"))]
((aget yay "sayHello") ((aget yay "getMessage"))))
Careful readers may notice above that we are referencing js/window
just as we did js/yayQuery
in the failing example. It works in this case because Google Closure Compiler ships out of the box with a number of externs for browser APIs. These are enabled by default.
To maximize efficiency of content delivery, you can bundle JavaScript code along with your compiled ClojureScript code.
If your external JavaScript code has been written to be compatible with Google Closure Compiler, and exposes its namespaces using goog.provide
, the most efficient way to include it is to bundle it using :libs
. This bundling mechanism takes full advantage of advanced mode compilation, renaming symbols in the external JavaScript library and eliminating dead code. Let's adapt our yayQuery library from previous examples, as below:
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;
};
This code is mostly identical to the previous inline version, but is now packaged within a "namespace" exposed using goog.provide
. The library can be referenced easily in ClojureScript:
(ns hello-world.core
(:require [yq]))
(let [yay (yq/yayQuery)]
(.sayHello yay (.getMessage yay)))
To build the bundled output, use the following in your build.clj
file.
(cljs.closure/build "src" {:output-to "out/main.js"
:libs ["yayquery.js"]
:optimizations :advanced})
Because this code is compatible with advanced compilation, there is no need to create externs. If you look at the compiled output, you'll see that the functions have been renamed and the unreferenced debugMessage
has been completely eliminated by Google Closure Compiler.
While an extremely efficient way to bundle external JavaScript, most popular libraries are not compatible with this approach.
If the code you wish to bundle has not been authored with Google Closure Compiler compatibility in mind, you can include it as a foreign library. Foreign libraries are included in your final output, but are not passed through advanced compilation. Let's consider a version of yayQuery which does not include a goog.provide
:
yayQuery = function() {
var yay = {};
yay.sayHello = function(message) {
console.log(message);
};
yay.getMessage = function() {
return 'Hello, world!';
};
return yay;
};
Using code in foreign libraries from ClojureScript is very similar to using code that's been included directly in the page via a <script>
tag, with one key difference:
(ns hello-world.core
(:require [yq]))
(let [yay (js/yayQuery)]
(.sayHello yay (.getMessage yay)))
Notice the presence of :require
in the ns
declaration. This references a "namespace" called yq
, but there is no corresponding goog.provide
in the yayQuery file. In the case of foreign libraries, the "namespace" is provided in the build configuration. As long as the name in the :provides
key matches what you :require
and is unique across referenced libraries, you can name it anything you please:
(cljs.closure/build "src" {:output-to "out/main.js"
:externs ["yayquery-externs.js"]
:foreign-libs [{:file "yayquery.js"
:provides ["yq"]}]
:optimizations :advanced})
Note that we have re-introduced our externs file here. Though the foreign library is bundled, it must otherwise be referenced exactly as if the script had been included externally.
The previous sections have discussed the various ways of integrating with any external JavaScript code. Finding the best way to integrate a library can be tricky, especially if you have to procure externs. Fortunately, for many of the most common JavaScript libraries, there is an easier way. The CLJSJS project automatically packages up external JavaScript libraries in a way that's directly supported by the ClojureScript compiler. It will automatically package the best version of a library in a given context (including minified libraries when using advanced optimizations, for example), and automatically includes the appropriate externs.
Let's say we've outgrown our beloved yayQuery library, and want to use jQuery instead. This is one of the many popular libraries which has been pre-packaged. We can fetch a copy as below:
curl -O https://clojars.org/repo/cljsjs/jquery/1.9.0-0/jquery-1.9.0-0.jar
If you take a peek inside the downloaded JAR file (unzip jquery-1.9.0-0.jar deps.cljs
), you'll see the contents of the bundled deps.cljs
file:
{: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"]}
If you followed along with the previous sections, this should all be quite clear at this point. The :provides
data tells us all we need to reference this code:
(ns hello-world.core
(:require [cljsjs.jquery]))
(.text (js/$ "body") "Hello, World!")
The build file in this case is incredibly simple, as the library reference is entirely contained in the JAR which we'll reference when we invoke the script:
(cljs.closure/build "src" {:output-to "out/main.js"
:optimizations :advanced})
Compile the code as below (note the addition of the JAR in our class path), and you should see the message display when you load your browser:
java -cp cljs.jar:jquery-1.9.0-0.jar:src clojure.main build.clj
How to compile ClojureScript JARs without external tooling.
Explanation of Maven/Leiningen.
- Rationale
- Quick Start
- Differences from Clojure
- [Usage of Google Closure](Google Closure)