Chapter 3. The Compilation Process

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.

Architecture

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.

Google Closure Compiler

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.

Note

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:

Whitespace Only

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.”

Simple Optimizations

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.

Advanced Optimizations

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.”

Note

Google makes a Closure Compiler demo application available for developers to experiment with the effects of different compilation modes.

The Google Closure Library

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 and Google Closure

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 Compilation Pipeline

The final picture of ClojureScript compilation looks like Figure 3-1.

ClojureScript Compilation Process
Figure 3-1. ClojureScript Compilation Process

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.

How to Compile

In this section, we will walk through the ClojureScript compilation process in detail, showing how the parts interact.

Compiling ClojureScript

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.

Hello, Compiler

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>

The Clojure REPL

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.

Compilation in Depth

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)

Compilation Sources

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.

Compilation and Optimization Options

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.

:optimizations

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 ValueGoogle Closure Compiler Mode
:none(disabled)
:whitespaceWhitespace-Only
:simpleSimple Optimizations
:advancedAdvanced 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).

Where do the files go?

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.

Compiling with optimizations

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.

Compiler inputs and outputs with optimization
Figure 3-2. Compiler inputs and outputs with optimization

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.

Loading optimized code in a browser

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.

Compiling without optimizations

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.

Compiler inputs and outputs without optimization
Figure 3-3. Compiler inputs and outputs without optimization

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".

Loading unoptimized code in a browser

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.

An alternative: pretty-printing

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.

Other Compilation Options

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.

Summary

All the compilation options to cljs.closure/build are summarized in Table 3-1.

Table 3-1. Compilation options
OptionPossible Values
:output-tofile path as a string
:output-dirdirectory path as a string
:optimizations:none, :whitespace, :advanced
:pretty-printfalse (default), true
:target(browsers by default), :nodejs
:libsSee Chapter 7.
:foreign-libsSee Chapter 7.
:externsSee Chapter 7.

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.118.189.251