ClojureScript has a tight symbiotic relationship with other tools. This chapter will explain how all the different parts fit together and then demonstrate the ClojureScript compilation process.
ClojureScript is a compiler—that is, a program that takes a “source” representation as input and emits a “target” representation as output. The source representation of the ClojureScript compiler is the ClojureScript language, and the target representation is JavaScript.
Unlike some JavaScript-generation tools and frameworks, ClojureScript itself does not do any “minification” or other optimizations to reduce the size of the JavaScript code it emits. Instead, ClojureScript is designed to work with the Google Closure Compiler to produce optimized JavaScript.
The Google Closure Compiler is a free, open-source compiler that uses JavaScript as both source and target representations. That is, it compiles JavaScript into JavaScript. Along the way, it can perform sophisticated optimizations to reduce the size and improve the runtime performance of JavaScript code.
The fact that “Clojure” and “Closure” are homophones is an unfortunate historical accident. The owners/authors of the two projects have no relationship to one another. In this book, we will always refer to the “Google Closure Compiler” and the “Google Closure Library” by their full names.
The Google Closure Compiler can run in three different modes:
This mode removes only comments and unnecessary whitespace from JavaScript source code. The target JavaScript is functionally identical to the source JavaScript. This is similar to some simple JavaScript “minifiers.”
This mode does all the same optimizations as Whitespace Only mode and further reduces the size of target JavaScript by renaming local variables and function parameters to shorter names.
This mode does all the same optimizations as the previous two modes and also performs aggressive whole-program optimizations of JavaScript code. It will completely remove “dead” or unreachable code, rename functions and global variables to shorter names, and even rename inline function bodies when doing so will save space.
While the more aggressive optimization modes of the compiler can dramatically reduce the size of JavaScript source code, they come with a few caveats. In order to perform the optimizations in Simple and Advanced modes, the Google Closure Compiler must make certain assumptions about the source JavaScript. If the source JavaScript code violates these assumptions, the Google Closure Compiler will produce target JavaScript code that does not work as intended.
For example, Simple Optimizations mode will
break JavaScript code that uses JavaScript’s with
operator,
eval
function, or any string representation of function or
parameter names. Advanced Optimizations mode is
even more restrictive: because it renames global variables and functions
to shorten their names, it will break any code that depends on names
being stable. For example, code that refers to object property names as
strings (like user["name"]
instead of
user.name
) will sometimes break under
Advanced mode.
The documentation for the Google Closure Compiler explains all the effects of Advanced Optimizations mode in detail. Essentially, using the Google Closure Compiler in Advanced mode requires that developers follow strict conventions for how their JavaScript code is structured. The JavaScript code that results from following these conventions often looks “unnatural” to developers accustomed to writing optimized JavaScript code by hand, but the final result produced by the Google Closure Compiler is generally just as or more efficient than hand-optimized JavaScript run through a “minifier.”
Google makes a Closure Compiler demo application available for developers to experiment with the effects of different compilation modes.
The Google Closure Compiler is distributed along with an extensive collection of free and open-source libraries, written in JavaScript, which follow all the conventions required by the compiler in Advanced Optimizations mode. These libraries include data structures, common algorithms, abstractions over browser quirks, and even a GUI toolkit. Because of the Advanced-mode conventions, the source code of these libraries may look “unnatural” to a JavaScript developer. The Google Closure Library code is written to target the Google Closure Compiler, so it is more verbose than most JavaScript written to target web browsers directly. Common by-hand JavaScript optimizations, such as using short names for common functions, do not matter in Advanced mode, because the compiler will rename everything anyway.
The Google Closure Library is much larger than most JavaScript libraries—several megabytes as opposed to a few hundred kilobytes. Again, a JavaScript developer accustomed to hand-optimized code would think this is grossly inefficient. But the Google Closure Compiler’s Advanced-mode optimizations ensure the actual code delivered in a production application is much smaller. Any “dead” library code not actually used by the application will be eliminated during compilation. In short, you only pay (in download size) for what you use.
ClojureScript is designed to work with the Google Closure Compiler and Library. The ClojureScript compiler emits JavaScript code that is fully compatible with the Advanced Optimizations mode of the Google Closure Compiler. As a result, when programming in ClojureScript you rarely need to think about the JavaScript conventions required by Advanced mode. Many of the core libraries included with ClojureScript make use of functions in the Google Closure Library.
Using ClojureScript does not mean that you are restricted to using code only in the Google Closure Library. ClojureScript can make use of any JavaScript library with a little additional configuration. However, most hand-optimized JavaScript libraries are not written with the Google Closure Compiler in mind, so they will not be compatible with Advanced Optimizations mode. ClojureScript can still use libraries such as jQuery or Prototype, but the libraries themselves will not receive the benefit of Advanced-mode compilation. Chapter 7 will cover using third-party JavaScript libraries in ClojureScript.
The final picture of ClojureScript compilation looks like Figure 3-1.
The entire compilation process happens inside a Java Virtual Machine (JVM), presumably running on a server or developer’s machine. The ClojureScript compiler is written in the Clojure language, which runs on the JVM. The Google Closure Compiler is written in the Java language.
The ClojureScript compiler takes ClojureScript source code and compiles it into unoptimized JavaScript, which it passes to the Google Closure Compiler along with JavaScript libraries. The Google Closure Compiler takes in all the unoptimized JavaScript and emits a single optimized JavaScript source file.
The JavaScript output by the Google Closure Compiler in Advanced Optimizations mode is intended for consumption by JavaScript execution engines, not humans. It is not readable and not very suitable for JavaScript debugging tools. When developing your application, it is more common to omit the Google Closure Compiler from the compilation process, which will result in readable JavaScript. Function and variable names in the emitted JavaScript can easily be correlated with sources in ClojureScript. Debugging support in ClojureScript still has room for improvement, but the process is already usable. In addition, ClojureScript has some unique debugging tools such as the browser-connected Read-Eval-Print-Loop (REPL), which we will cover in Chapter 9.
In this section, we will walk through the ClojureScript compilation process in detail, showing how the parts interact.
The entire ClojureScript build chain, including the ClojureScript compiler and the Google Closure Compiler, can be invoked as a single function in Clojure. In this section, we will use the Clojure REPL to explore the various options of the ClojureScript compiler. We’ll use a variant of the “Hello, World” example from Chapter 2. Instead of using lein-cljsbuild, this example will invoke the ClojureScript compiler directly. This process is unlikely to become part of your day-to-day development workflow, but it is helpful to understand how the parts work. You can also use this section as a guide to incorporating ClojureScript into customized builds.
Create a new project like this:
lein new ch03-hello-compiler
Then modify the project.clj
file to look like
this:
(defproject ch03-hello-compiler "0.1.0-SNAPSHOT" :dependencies [[org.clojure/clojure "1.4.0"] [org.clojure/clojurescript "0.0-1450"]] :source-paths ["src/clj"])
Create the src/clj
and src/cljs
directories as in Chapter 2, then put the
following ClojureScript source file in
src/cljs/hello_compiler/hello.cljs
:
(ns hello-compiler.hello) (defn ^:export main [] (.write js/document "<p>Hello, ClojureScript compiler!</p>"))
Finally, create an HTML file at public/resources/index.html:
<!DOCTYPE html> <html> <head><title>ClojureScript Hello Compiler</title></head> <body> <script src="hello.js" type="text/javascript"></script> <script>hello_compiler.hello.main()</script> </body> </html>
Both Clojure and ClojureScript have their own REPLs. In this chapter, we are going to invoke the ClojureScript compiler, which is implemented in Clojure, so we will be using the Clojure REPL. In your new project, you can launch the Clojure REPL by running:
lein repl
Then type the following to load the ClojureScript compiler:
(require 'cljs.closure)
Then type the following (long) expression to compile your project with the Google Closure Compiler in Advanced Mode:
(cljs.closure/build "src/cljs" {:output-to "resources/public/hello.js" :optimizations :advanced})
The Advanced Mode optimizations are
time-consuming: this simple build may take 20 seconds or more. When it
finishes, you will have an optimized JavaScript source file at
resources/public/hello.js
. Compare the size of this file
with the unoptimized file you created in Chapter 2—the optimized JavaScript emitted by the
Google Closure Compiler is much smaller.
When you type (cljs.closure/build ...)
in the Clojure
REPL you are invoking a function. The entire function call is wrapped in
parentheses. The cljs.closure/build
function takes two
arguments, a source and a map of
options:
(cljs.closure/build source options-map)
The source argument tells the compiler where
to find our ClojureScript source files. Typically, it is the name of a
directory, given as a string. The compiler will find all files with the
.cljs
extension in that directory and compile them
together.
The source argument can also be the name of a single file to be compiled. This might be useful during development, when you only want to recompile part of a project.
The options are passed to the
cljs.closure/build
function in a Clojure
map, written as a series of pairs inside curly
braces.
In the previous example, we passed two options:
:output-to "resources/public/hello.js" :optimizations :advanced
The words that begin with colons are keywords, a special kind of literal data in Clojure and ClojureScript. For our purposes, they act like constants.
We have already seen two possible values for the
:optimizations
option, in this and the previous chapter.
This option controls the optimization mode in which to run the Google
Closure Compiler.
:optimizations Value | Google Closure Compiler Mode |
:none | (disabled) |
:whitespace | Whitespace-Only |
:simple | Simple Optimizations |
:advanced | Advanced Optimizations |
With an :optimizations
value of :none
,
the Google Closure Compiler will not be invoked at all, and the build
will write out the JavaScript produced by the ClojureScript compiler
directly. This mode is useful for development and debugging. However,
the JavaScript output will be split across many individual files,
requiring slightly different handling in a browser (more on this
later).
The ClojureScript compiler produces one JavaScript file for each
ClojureScript source file. These files go in a directory controlled by
the :output-dir
option, which defaults to a directory
named out
in the current working directory. The current
working directory is whatever directory the Java (or Leiningen)
process was started in. The JVM does not support changing the current
working directory once a program has started.
The Google Closure Compiler is designed to optimize JavaScript
for delivery over slow networks. As a consequence, it always produces
a single JavaScript file for the entire compiled
application. When any one of the optimization modes is enabled, the
output of cljs.closure/build
will always be a single
JavaScript file.
Figure 3-2 shows the behavior of the
cljs.closure/build
function when compiling with
optimizations. The :output-dir
option controls where the
ClojureScript compiler writes intermediate files. The
:output-to
option specifies the file location of the
final output from the Google Closure Compiler. When you are compiling
your application for production use, this is the JavaScript file you
would put on your web server and reference in HTML pages.
If you do not specify an output file, the
cljs.closure/build
function simply returns the compiled
JavaScript source code as one giant string. This might be interesting
if you want to understand how the compiler works, but it’s still going
to be a big blob of your entire application, so it’s probably not
useful.
To run your optimized code in a browser, simply include the
:output-to
file in a <script>
tag,
like this:
<script src="hello.js" type="text/javascript"></script>
ClojureScript programs usually do not act like “scripts” in the
conventional sense. Loading the compiled JavaScript does not
do anything except define functions. You
typically launch your application with a “main” or “start” function
invoked in a separate <script>
tag, like
this:
<script> hello_compiler.hello.main(); </script>
The details of how the ClojureScript function names translate to JavaScript object names will be covered in more detail in Chapter 7, but the short version is that hyphens become underscores.
When you specify :optimizations :none
the Google
Closure Compiler does not run at all (Figure 3-3).
But the :output-to
option is still important.
The Google Closure Library includes a dependency-resolution
feature that makes it possible to split a JavaScript application
across many source files and automatically load the right files in a
web browser. This mechanism will be covered in detail in Chapter 7. For now, just
know that the dependency resolution mechanism requires a special file
that declares all the dependency relationships in your source code.
When compiling without optimizations, the
ClojureScript compiler writes this information to the file specified
by the :output-to
option.
In order for a browser to load the individual files, the
:output-dir
option must be set to a directory that
you can reference in the <script>
tag of an
HTML file. In our examples, the convention is
"resources/public/js"
.
To run your application in a browser without optimizations, you
need four <script>
tags in
your HTML, in precisely this order:
<script src="js/goog/base.js"></script> <script src="hello.js"></script> <script> goog.require('hello_compiler.hello'), </script> <script> hello_compiler.hello.main(); </script>
The first <script>
tag loads the Google
Closure Library from goog/base.js
, which will be found in
the directory specified by the :output-dir
option.
The second <script>
tag loads the dependency
information for your application from the file specified by the
:output-to
option.
The third <script>
tag uses the Google
Closure Library function goog.require
to load your
application. The argument to goog.require
is a JavaScript
string naming the primary namespace of your
application. Namespaces will be fully covered in Chapter 7, but you have
already seen them in all of the code examples. The ClojureScript
expression (ns hello-compiler.hello)
declares a namespace
named hello-compiler.hello
. Once again, hyphens become
underscores in JavaScript, yielding
hello_compiler.hello
.
The fourth <script>
tag launches your
application, the same as in the optimized case. Because of the way
goog.require
works, the code to launch your application
must be in a separate <script>
tag coming after the <script>
that calls goog.require
.
In general, you will compile your ClojureScript application for
production with :optimizations :advanced
, and for
development with :optimizations :none
. But there is a
third way, which is to use :optimizations :whitespace
and
also add the :pretty-print true
option. This combination
will still combine all of your JavaScript into a single source file
and invoke the Google Closure Compiler, but it will reformat the
JavaScript code for maximum readability.
The compilation process with :optimizations
:whitespace
and :pretty-print true
takes slightly
longer than with :optimizations :none
, but it has the
advantage of being simpler to use. You can use the exact same HTML
<script>
tags that you would use for
fully-optimized production code, but you can still read and debug the
JavaScript code directly in the browser.
The pretty-printing feature is provided by the Google Closure
Compiler, so it has no effect with :optimizations
:none
.
The default target for the ClojureScript compiler is web browsers.
The compiler can also be used to emit JavaScript code for other
execution environments, such as Node.js.[1] Passing the option :target :nodejs
to
cljs.closure/build
will tell the ClojureScript compiler to
emit code, which is compatible with Node.js.
Compiling ClojureScript for Node.js is still an
experimental feature and not widely used, so we do not cover it in this
book.
The :libs
, :foreign-libs
, and
:externs
options control access to external JavaScript
libraries; these will be covered in Chapter 7.
All the compilation options to cljs.closure/build
are
summarized in Table 3-1.
This chapter explained the high-level architecture ClojureScript compiler and its relationship with the Google Closure Compiler. We showed how to launch the Clojure and ClojureScript REPLs and how to invoke the ClojureScript compiler.
In subsequent chapters we will delve into the ClojureScript language itself. The Clojure/ClojureScript REPL shown in Chapter 2 and Chapter 3 should be sufficient to follow along with the examples that follow. After covering the language, we will circle back to compilation and development workflow in more detail.
18.118.189.251