- 1. Introduction
- 2. Installation
- 3. Usage
- 4. REPL
- 5. Configuration
- 6. Build Configuration
- 7. Targeting the Browser
- 8. Targeting JavaScript Modules
- 9. Targeting React Native
- 10. Targeting node.js
- 11. Embedding in the JS Ecosystem The :npm-module Target
- 12. Testing
- 13. JavaScript Integration
- 14. Generating Production Code All Targets
- 15. Editor Integration
- 16. Troubleshooting
- 17. Publishing Libraries
- 18. What to do when things don’t work?
- 19. Hacking
7. Targeting the Browser
The :browser
target produces output intended to run in a Browser environment. During development it supports live code reloading, REPL, CSS reloading. The release
output will be minified by the Closure Compiler with :advanced
optimizations.
A basic browser configuration looks like this:
{:dependencies [...]
:source-paths [...]
:builds
{:app {:target :browser
:output-dir "public/assets/app/js"
:asset-path "/assets/app/js"
:modules {:main {:entries [my.app]}}}}}
7.1. Output Settings
The browser target outputs a lot of files, and a directory is needed for them all. You’ll need to serve these assets with some kind of server, and the Javascript loading code needs to know the server-centric path to these assets. The options you need to specify are:
:output-dir
The directory to use for all compiler output.
:asset-path
The relative path from web server’s root to the resources in
:output-dir
.
Your entry point javascript file and all related JS files will appear in :output-dir
.
Warning | Each build requires its own :output-dir, you may not put multiple builds into the same directory. This directory should also be exclusively owned by the build. There should be no other files in there. While shadow-cljs won’t delete anything it is safer to leave it alone. Compilation creates many more files than just the main entry point javascript file during development: source maps, original sources, and generated sources. |
The :asset-path
is a prefix that gets added to the paths of module loading code inside of the generated javascript. It allows you to output your javascript module to a particular subdirectory of your web server’s root. The dynamic loading during development (hot code reload) and production (code splitting) need this to correctly locate files.
Locating your generated files in a directory and asset path like this make it so that other assets (images, css, etc.) can easily co-exist on the same server without accidental collisions.
For example: if your web server will serve the folder public/x
when asked for the URI /x
, and your output-dir
for a module is public/assets/app/js
then your asset-path should be /assets/app/js
. You are not required to use an absolute asset path, but it is highly recommended.
7.2. Modules
Modules configure how the compiled sources are bundled together and how the final .js
are generated. Each Module declares a list of Entry Namespace and from that dependency graph is built. When using multiple Modules the code is split so that the maximum amount of code is moved to the outer edges of the graph. The goal is to minimize the amount of code the browser has to load initially and loading the rest on-demand.
Tip | Don’t worry too much about :modules in the beginning. Start with one and split them later. |
The :modules
section of the config is always a map keyed by module ID. The module ID is also used to generate the Javascript filename. Module :main
will generate main.js
in :output-dir
.
The available options in a module are:
:entries | The namespaces that serve as the root nodes of the dependency graph for the output code of this module. |
:init-fn | Fully qualified symbol pointing to a function that should be called when the module is loaded initially. |
:depends-on | The names of other modules that must be loaded in order for this one to have everything it needs. |
:prepend | String content that will be prepended to the js output. Useful for comments, copyright notice, etc. |
:append | String content that will be appended to the js output. Useful for comments, copyright notice, etc. |
:prepend-js | A string to prepend to the module output containing valid javascript that will be run through Closure optimizer. |
:append-js | A string to append to the module output containing valid javascript that will be run through Closure optimizer. |
The following example shows a minimum module configuration:
Example :browser config{...
:builds
{:app {:target :browser
:output-dir "public/js"
...
:modules {:main {:entries [my.app]}}}}}
Example :browser config with :init-fn{...
:builds
{:app {:target :browser
:output-dir "public/js"
...
:modules {:main {:init-fn my.app/init}}}}}
shadow-cljs
will follow the dependency graph from the root set of code entry points in the :entries
to find everything needed to actually compile and include in the output. Namespaces that are not required will not be included.
The above config will create a public/js/main.js
file. During development there will be an additional public/js/cljs-runtime
directory with lots of files. This directory is not required for release
builds.
7.3. Code Splitting
Declaring more than one Module requires a tiny bit of additional static configuration so the Compiler can figure out how the Modules are related to each other and how you will be loading them later.
In addition to :entries
you’ll need to declare which module depends on which (via :depends-on
). How you structure this is entirely up to your needs and there is no one-size-fits-all solution unfortunately.
Say you have a traditional website with actual different pages.
www.acme.com
- serving the homepagewww.acme.com/login
- serving the login formwww.acme.com/protected
- protected section that is only available once the user is logged in
One possible configuration for this would be to have one common module that is shared between all the pages. Then one for each page.
Example config with multiple:modules
{...
:output-dir "public/js"
:modules
{:shared
{:entries [my.app.common]}
:home
{:entries [my.app.home]
:depends-on #{:shared}}
:login
{:entries [my.app.login]
:depends-on #{:shared}}
:protected
{:entries [my.app.protected]
:depends-on #{:shared}}
Tip | You can leave the :entries of the :shared module empty to let the compiler figure out which namespaces are shared between the other modules. |
.
└── public
└── js
├── shared.js
├── home.js
├── login.js
└── protected.js
In your HTML for the Homepage you’d then always include the shared.js
on each page and the others conditionally depending on which page the user is on.
/login
page<script src="/js/shared.js"></script>
<script src="/js/login.js"></script>
Important | The .js files must be included in the correct order. The manifest.edn can help with this. |
7.3.1. Loading code dynamically
The more dynamic your website gets, the more dynamic your requirements may get. The server may not always know what the client may end up needing. Therefore, it is possible to have the client load code dynamically when needed.
There are a couple ways of loading code dynamically. shadow.lazy
is the most convenient and easiest.
Using shadow.lazy
As announced here shadow-cljs provides a convenience method for referring to potentially lazy loaded code.
(ns demo.app
(:require
[shadow.lazy :as lazy]
[shadow.cljs.modern :refer (js-await)]))
(def x-lazy (lazy/loadable demo.thing/x))
(defn on-event [e]
(js-await [x (lazy/load x-lazy)]
(x e)))
Let’s assume that the on-event
function above is called when something in your app happens, for example when the user clicked a button. The lazy/loadable
configured what that thing will be. The lazy/load
will actually load it. This may require an async network hop, so it will go async at this point. In the body of the js-await
above x
will be whatever demo.thing/x
was at the time of loading it.
(ns demo.thing)
(defn x [e]
"hello world")
In this case it would be the function, which we can call directly.
You do not need to worry about specifying which module this code ended up in. The compiler will figure that out during compilation. The loadable
macro also allows more complex references.
(def xy (lazy/loadable [demo.thing/x demo.other/y]))
(def xym (lazy/loadable {:x demo.thing/x
:y demo.other/y}))
If you load xy
the result will be a vector with two things. If you load xym
it’ll be a map. You may include vars that span multiple modules that way. The loader will ensure all modules are loaded before continuing.
Using shadow-cljs’s built-in Loader Support
Important | This is the low level version, which the above is built upoin. Use it if you want to build your own abstraction for async loading. The above is much more convenient to use. |
The compiler supports generating the required data for using the shadow.loader
utility namespace. It exposes a simple interface to let you load modules on-demand at runtime.
You only need to add :module-loader true
to your build config. The loader will always be injected into the default module (the one everything else depends on).
At runtime you may use the shadow.loader
namespace to load modules. You may also still load a module eagerly by just using a <script>
tag in your page.
{...
:builds
{:app
{:target :browser
...
:module-loader true
:modules {:main {:entries [my.app]}
:extra {:entries [my.app.extra]
:depends-on #{:main}}}}}}
If you had the following for your main entry point:
(ns my.app
(:require [shadow.loader :as loader]))
(defn fn-to-call-on-load []
(js/console.log "extra loaded"))
(defn fn-to-call-on-error []
(js/console.log "extra load failed"))
Then the following expressions can be used for loading code:
Loading a module;; load returns a goog.async.Deferred, and can be used like a promise
(-> (loader/load "extra")
(.then fn-to-call-on-load fn-to-call-on-error))
Loading many modules;; must be a JS array, also returns goog.async.Deferred
(loader/load-many #js ["foo" "bar"])
Including a callback(loader/with-module "extra" fn-to-call-on-load)
You can check if a module is loaded using (loaded? "module-name")
.
You can read more about a more practical example in this blog post about Code-Splitting ClojureScript. This is only a basic overview.
Loader Costs
Using the loader is very lightweight. It has a few dependencies which you may not be otherwise using. In practice using :module-loader true
adds about 8KB gzip’d to the default module. This will vary depending on how much of goog.net
and goog.events
you are already using, and what level of optimization you use for your release builds.
Using the Standard ClojureScript API
The generated code is capable of using the standard ClojureScript cljs.loader
API. See the documentation on the ClojureScript website for instructions.
The advantage of using the standard API is that your code will play well with others. This may be of particular importance to library authors. The disadvantage is that the dynamic module loading API in the standard distribution is currently somewhat less easy-to-use than the support in shadow-cljs
.
7.4. Output Wrapper
Release builds only: The code generated by the Closure Compiler :advanced
compilation will create a lot of global variables which has the potential to create conflicts with other JS running in your page. To isolate the created variables the code can be wrapped in an anonymous function to the variables only apply in that scope.
release
builds for :browser
with only one :modules
are wrapped in (function(){<the-code>}).call(this);
by default. So no global variables are created.
When using multiple :modules
(a.k.a code splitting) this is not enabled by default since each module must be able to access the variables created by the modules it depends on. The Closure Compiler supports an additional option to enable the use of an output wrapper in combination with multiple :modules
named :rename-prefix-namespace
. This will cause the Compiler to scope all "global" variables used by the build into one actual global variable. By default this is set to :rename-prefix-namespace "$APP"
when :output-wrapper
is set to true
.
{...
:builds
{:target :browser
...
:compiler-options
{:output-wrapper true
:rename-prefix-namespace "MY_APP"}}}
This will only create the MY_APP
global variable. Since every "global" variable will now be prefixed by MY_APP.
(e.g. MY_APP.a
instead of just a
) the code size can go up substantially. It is important to keep this short. Browser compression (e.g. gzip
) helps reduce the overhead of the extra code but depending on the amount of global variables in your build this can still produce a noticeable increase.
Important | Note that the created variable isn’t actually useful directly. It will contain a lot of munged/minified properties. All exported (eg. ^:export ) variables will still be exported into the global scope and are not affect by this setting. The setting only serves to limit the amount of global variables created, nothing else. Do not use it directly. |
7.5. Web Workers
The :modules
configuration may also be used to generate files intended to be used as a Web Workers. You may declare any module as a Web Worker by setting :web-worker true
. The generated file will contain some additional bootstrap code which will load its dependencies automatically. The way :modules
work also ensures that code used only by the worker will also only be in the final file for the worker. Each worker should have a dedicated CLJS namespace.
{...
:builds
{:app
{:target :browser
:output-dir "public/js"
:asset-path "/js"
...
:modules
{:shared
{:entries []}
:main
{:init-fn my.app/init
:depends-on #{:shared}}
:worker
{:init-fn my.app.worker/init
:depends-on #{:shared}
:web-worker true}}
}}}
The above configuration will generate worker.js
which you can use to start the Web Worker. It will have all code from the :shared
module available (but not :main
). The code in the my.app.worker
namespace will only ever execute in the worker. Worker generation happens in both development and release modes.
Note that the empty :entries []
in the :shared
module will make it collect all the code shared between the :main
and :worker
modules.
(ns my.app.worker)
(defn init []
(js/self.addEventListener "message"
(fn [^js e]
(js/postMessage (.. e -data)))))
Sample using the worker(ns my.app)
(defn init []
(let [worker (js/Worker. "/js/worker.js")]
(.. worker (addEventListener "message" (fn [e] (js/console.log e))))
(.. worker (postMessage "hello world"))))
Important | Since we now have a :shared module you must ensure to load it properly in your HTML. If you just load main.js you will get an error. |
<script src="/js/shared.js"></script>
<script src="/js/main.js"></script>
7.6. Cacheable Output
In a web setting it is desirable to cache .js
files for a very long time to avoid extra request. It is common practice the generate a unique name for the .js
file for every released version. This changes the URL used to access it and thereby is safe to cache forever.
7.6.1. Release Versions
Creating unique filenames for each release can be done via the :release-version
config setting. Generally you’ll pass this in from the command line via --config-merge.
shadow-cljs release app --config-merge '{:release-version "v1"}'
Example :modules config{...
:builds
{:app
{:target :browser
...
:output-dir "public/js"
:asset-path "/js"
:modules {:main {:entries [my.app]}
:extra {:entries [my.app.extra]
:depends-on #{:main}}}}}}
This would create the main.v1.js
and extra.v1.js
files in public/js
instead of the usual main.js
and extra.js
.
You can use manual versions or something automated like the git
sha at the time of the build. Just make sure that you bump whatever it is once you shipped something out to the user since with caching they won’t be requesting newer versions of old files.
7.6.2. Filenames with Fingerprint-Hash
You can add :module-hash-names true
to your build config to automatically create a MD5 signature for each generated output module file. That means that a :main
module will generate a main.<md5hash>.js
instead of just the default main.js
.
:module-hash-names true
will include the full 32-length md5 hash, if you prefer a shorter version you can specify a number between 1-32 instead (eg. :module-hash-names 8
). Be aware that shortening the hash may increase the chances of generating conflicts. I recommend using the full hash.
{...
:builds
{:app
{:target :browser
...
:output-dir "public/js"
:asset-path "/js"
:module-hash-names true
:modules {:main {:entries [my.app]}
:extra {:entries [my.app.extra]
:depends-on #{:main}}}}}}
Instead of generating main.js
it will now generate main.<hash>.js
in the :output-dir
.
Since the filename can change with every release it gets a little bit more complicated to include them in your HTML. If you want to accomplish this with a program external to shadow-cljs, you can find programmatic information about filenames in the Output Manifest. If you want to accomplish this as part of the build, have a look at Build Hooks, and at this hook specifically for inspiration.
7.7. Output Manifest
shadow-cljs
generates a manifest.edn
file in the configured :output-dir
. This file contains a description of the module config together with an extra :output-name
property which maps the original module name to actual filename (important when using the :module-hash-names
feature).
[{:module-id :common,
:name :common,
:output-name "common.15D142F7841E2838B46283EA558634EE.js",
:entries [...],
:depends-on #{},
:sources [...]}
{:module-id :page-a,
:name :page-a,
:output-name "page-a.D8844E305644135CBD5CBCF7E359168A.js",
:entries [...],
:depends-on #{:common},
:sources [...]}
...]
The manifest contains all :modules
sorted in dependency order. You can use it to map the :module-id
back to the actual generated filename.
Development builds also produce this file and you may check if for modifications to know when a new build completed. :module-hash-names
does not apply during development so you’ll get the usual filenames.
You can configure the name of the generated manifest file via the :build-options :manifest-name
entry. It defaults to manifest.edn
. If you configure a filename with .json
ending the output will be JSON instead of EDN. The file will be relative to the configured :output-dir
.
{...
:builds
{:app
{:target :browser
...
:build-options {:manifest-name "manifest.json"}
:modules {:main {:entries [my.app]}
:extra {:entries [my.app.extra]
:depends-on #{:main}}}}}}
7.8. Development Support
The :devtools
section of the configuration for :browser
supports a few additional options for configuring an optional dev-time HTTP server for a build and CSS reloading.
7.8.1. Heads-Up Display (HUD)
The :browser
target now uses a HUD to display a loading indicator when a build is started. It will also display warnings and errors if there are any.
You can disable it completely by setting :hud false
in the :devtools
section.
You may also toggle certain features by specifying which features you care about via setting :hud #{:errors :warnings}
. This will show errors/warnings but no progress indicator. Available options are :errors
, :warnings
, :progress
. Only options included will be enabled, all other will be disabled.
Opening Files
Warnings include a link to source location which can be clicked to open the file in your editor. For this a little bit of config is required.
You can either configure this in your shadow-cljs.edn
config for the project or globally in your home directory under ~/.shadow-cljs/config.edn
.
:open-file-command
configuration{:open-file-command
["idea" :pwd "--line" :line :file]}
The :open-file-command
expects a vector representing a very simple DSL. Strings are kept as they are and keyword are replaced by their respective values. A nested vector can be used in case you need to combine multiple params, using clojure.core/format
style pattern.
The above example would execute
$ idea /path/to/project-root --line 3 /path/to/project-root/src/main/demo/foo.cljs
emacsclient
example{:open-file-command
["emacsclient" "-n" ["+%s:%s" :line :column] :file]}
$ emacsclient -n +3:1 /path/to/project-root/src/main/demo/foo.cljs
The available replacement variables are:
:pwd
Process Working Directory (aka project root)
:file
Absolute File Path
:line
Line Number of Warning/Error
:column
Column Number
:wsl-file
Translated WSL file path. Useful when running
shadow-cljs
via WSL Bash. Translates a/mnt/c/Users/someone/code/project/src/main/demo/foo.cljs
path intoC:\Users...
:wsl-pwd
Translated
:pwd
7.8.2. CSS Reloading
The Browser devtools can also reload CSS for you. This is enabled by default and in most cases requires no additional configuration when you are using the built-in development HTTP servers.
Any stylesheet included in a page will be reloaded if modified on the filesystem. Prefer using absolute paths but relative paths should work as well.
Example HTML snippet<link rel="stylesheet" href="/css/main.css"/>
Example Hiccup since we aren’t savages[:link {:rel "stylesheet" :href "/css/main.css"}]
Using the built-in dev HTTP server:dev-http {8000 "public"}
This will cause the browser to reload /css/main.css
when public/css/main.css
is changed.
shadow-cljs
currently provides no support for directly compiling CSS but the usual tools will work and should be run separately. Just make sure the output is generated into the correct places.
When you are not using the built-in HTTP Server you can specify :watch-dir
instead which should be a path to the document root used to serve your content.
{...
{:builds
{:app {...
:devtools {:watch-dir "public"}}}}
When your HTTP Server is serving the files from a virtual directory and the filesystem paths don’t exactly match the path used in the HTML you may adjust the path by setting :watch-path
which will be used as a prefix.
public/css/main.css
being served under /foo/css/main.css
{...
{:builds
{:app
{...
:devtools {:watch-dir "public"
:watch-path "/foo"}}}}
7.8.3. Proxy Support
By default the devtools client will attempt to connect to the shadow-cljs
process via the configured HTTP server (usually localhost
). If you are using a reverse proxy to serve your HTML that might not be possible. You can set :devtools-url
to configure which URL to use.
{...
:builds
{:app {...
:devtools {:before-load my.app/stop
:after-load my.app/start
:devtools-url "https://some.host/shadow-cljs"
...}}}}
shadow-cljs
will then use the :devtools-url
as the base when making requests. It is not the final URL so you must ensure that all requests starting with the path you configured (eg. /shadow-cljs/*
) are forwarded to the host shadow-cljs
is running on.
https://some.host/shadow-cljs/ws/foo/bar?asdf
must forward tohttp://localhost:9630/foo/bar?asdf
The client will make WebSocket request as well as normal XHR requests to load files. Ensure that your proxy properly upgrades WebSockets.
Important | The requests must be forwarded to the main HTTP server, not the one configured in the build itself. |
7.9. Using External JS Bundlers
Sometimes npm packages you may wish to use may use features that shadow-cljs
itself does not support. Some packages are even written with the explicit expectation to be processed by webpack
. In these cases it might be simpler to just use webpack
(or similar), instead of letting shadow-cljs
try to bundle those packages and working arround the issues that may occur.
shadow-cljs
supports an option to let it focus on compiling CLJS code, but let something else process the npm/JS requires. You can do so via :js-provider :external
. I wrote more on this subject in this blogpost.
Important | This will limit certain dynamic interaction. Adding new npm requires will require reloading the page, since they can no longer be hot-loaded in by shadow-cljs. Requiring npm packages at the REPL will also be limited to those already provided by the external JS file. Which often is not a big deal, but something to be aware of. |
In your build config you add:
{:builds
{:app
{:target :browser
:output-dir "..."
:modules {:main {...}}
:js-options
{:js-provider :external
:external-index "target/index.js"}}}}
shadow-cljs
will then just output all required npm
package requires in a format that regular JS tools can understand. You’ll then need to run webpack
(or similar) manually, and include the output of that build separately from the shadow-cljs
output.
So, instead of just including one script
tag in your HTML, you include two.
<script defer src="/js/libs.js"></script>
<script defer src="/js/main.js"></script>
With libs.js
here presuming to be the output of webpack
.
Important | Note that webpack (or similar) sometimes output more than one file, so which exactly you need to include may depend on how you built everything. Please consult their documentation for more details. The only important part as far as shadow-cljs is concerned that the external output is loaded before the shadow-cljs output. |
7.9.1. JS Tree Shaking
Tools like webpack
can potentially tree-shake npm dependencies to make their build output smaller. For this the :external-index
file needs to generate ESM code, instead of the current default CommonJS, i.e. require()
.
{:builds
{:app
{:target :browser
:output-dir "..."
:modules {:main {...}}
:js-options
{:js-provider :external
:external-index "target/index.js"
:external-index-format :esm}}}}
This will only use import
in the :external-index
file. For release
builds this file will list all the referenced imports, for watch/compile it’ll still reference everything.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论