Skip to content

Instantly share code, notes, and snippets.

@swannodette
Last active June 13, 2018 07:31
Show Gist options
  • Save swannodette/6150d4213aeb9eba31e03ae522af4425 to your computer and use it in GitHub Desktop.
Save swannodette/6150d4213aeb9eba31e03ae522af4425 to your computer and use it in GitHub Desktop.

Code Splitting

NOTE: This gist uses the master branch of ClojureScript. Clone ClojureScript and from the checkout run ./script/bootstrap and ./script/uberjar. This will produce target/cljs.jar which you can use to follow this guide.

As client applications become larger it becomes desirable to load only the code actually required to run a particular logical screen. Previously ClojureScript :modules compiler option permitted such code splitting, but this feature only worked under :advanced compilation and users would still have to manage loading these splits. :modules also required manual explicit placement of many entries to produce optimal splits otherwise dependencies would get moved to :cljs-base.

All of these issues are now addressed directly in ClojureScript. This guide will walk you through code splitting a simple project and demonstrate these new enhancements.

Make a Simple Project

Create a project folder:

mkdir -p hello-modules
cd hello-modules
mkdir src

Copy the cljs.jar into this directory.

Create a REPL script:

touch repl.clj

Edit this script to look like the following:

(require '[cljs.repl :as r])
(require '[cljs.build.api :as b])
(require '[cljs.repl.browser :as rb])

(def opts
  {:watch "src"
   :output-dir "out"
   :asset-path "/out"
   :optimizations :none
   :modules {:foo {:entries '#{foo.core}
                   :output-to "out/foo.js"}
             :bar {:entries '#{bar.core}
                   :output-to "out/bar.js"}}
   :browser-repl true
   :verbose true})

(b/build "src" opts)
(r/repl* (rb/repl-env) opts)

Now make an index.html file:

<html>
    <body>
         <button id="button">Load Bar!</button>
         <script src="out/cljs_base.js" type="text/javascript"></script>
         <script src="out/foo.js" type="text/javascript"></script>
    </body>
</html>

The Sources

Create the foo.core namespace:

mkdir -p src/foo
touch src/foo/core.cljs

Edit this file to look like the following:

(ns foo.core
  (:require [goog.dom :as gdom] 
            [goog.events :as events]
            [cljs.loader :as loader])
  (:import [goog.events EventType]))

(enable-console-print!)

(println "I'm foo!")

(events/listen (gdom/getElement "button") EventType.CLICK
  (fn [e]
    (loader/load :bar
      (fn []
        ((resolve 'bar.core/woz))))))

Notice the unfamiliar namespace cljs.loader. This namespace provides a Google Closure ModuleManager singleton to manage the loading of code splits. This manager will be initialized with whatever module graph you have defined in :modules.

When the user clicks the button we load the :bar module and invoke a function that exists in the bar.core namespace. Notice that we use resolve. This is because we cannot directly call something we never required. If we tried to do this without resolve the ClojureScript compiler would emit an undeclared var warning during compilation.

Create the bar.core namespace:

mkdir -p src/bar
touch src/bar/core.clj
(ns bar.core
  (:require [cljs.loader :as loader]))

(enable-console-print!)

(println "I'm bar!")

(defn woz []
  (println "WOZ!"))

Build the Project

Run the REPL script:

rlwrap java -cp cljs.jar:src clojure.main repl.clj

You will be prompted to connect via browser. Navigate to http://localhost:9000/index.html.

Click the button. You will see that the :bar module gets loaded and the function in the other namespace gets invoked.

Release Builds

Make a new script release.clj:

touch release.clj

Make it look like the following:

(require '[cljs.build.api :as b])

(b/build "src"
  {:output-dir "out"
   :asset-path "/out"
   :optimizations :advanced
   :verbose true
   :modules {:foo {:entries '#{foo.core}
                   :output-to "out/foo.js"}
             :bar {:entries '#{bar.core}
                   :output-to "out/bar.js"}}})

(System/exit 0)

Build your project:

java -cp cljs.jar:src clojure.main release.clj

Start a browser REPL just for serving content from current directory:

java -jar cljs.jar -m cljs.repl.browser

Navigate to http://localhost:9000/index.html. Your application should function correctly even though advanced compiled.

Change the foo.core to take a new require like cljs.reader. Rebuild.

You should see that cljs.reader gets moved into the :foo module but not :bar.

If you examine the split files in out you will see that foo.js is larger than bar.js.

@daonsh
Copy link

daonsh commented Nov 10, 2017

Hi,

What if the bar module needs access to some shared data from the foo module?

Thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment