返回介绍

8. Targeting JavaScript Modules

发布于 2023-07-23 16:03:27 字数 9090 浏览 0 评论 0 收藏 0

The :target :esm emits files that can be used in any ESM environment.

ESM, short for ECMAscript Modules, or just JavaScript Modules, is the modernized standard for JavaScript files. Most modern platforms support this out of the box, and more and more of the JS ecosystem is moving this way. Each Module generated this way can specify "exports" which other files importing this can reference.

ESM much like the :target :browser is driven by the :modules config option. Each module can declare its :exports for others to access.

As with other modes the main configuration options apply and can be set. The additional target-specific options are:

:output-dir

(optional, default "public/js"). The path where all :modules are written to.

:runtime

(optional , default :browser) Controls which development extensions for REPL/hot-reload are injected. Currently only supports :browser. Set to any other value to disable.

:modules

required, a map of keyword to module configuration

8.1. Module Configuration

Each module has its own set of options that control how the module is constructed. Specifying multiple modules will mean that the code is split between them.

:init-fn

optional, a defn to run when the module loads.

:entries

optional, a vector of namespace symbols to load in this module

:exports

required, a map of symbol to fully qualified symbols

:depends-on

required when using multiple :modules. specifing a set of other modules this module depends on.

8.1.1. Module Exports

Controlling what code is actually exported is done via :exports.

Example Build Config
{:source-paths ["src/main"]
 :dev-http {8000 "public"}
 :builds
 {:app
  {:target :esm
   :output-dir "public/js"
   :modules {:demo {:exports {hello demo.lib/hello}}}}}}

This will generate the public/js/demo.js file. The name is decided by taking the :output-dir and combining it with the key :demo in the :modules map.

Example CLJS Code
(ns demo.lib)
(defn hello []
  (js/console.og "hello world"))
(defn not-exported []
  (js/console.log "foo"))

It will be loadable directly in any ESM environment. For example the Browser. Putting this into a public/index.html and loading it via http://localhost:8000.

<script type="module">
  import { hello } from "/js/demo.js";
  hello();
</script>

With npx shadow-cljs watch app you should see the hello world logged to the browser console when loading the page.

Note that only hello is accessible here since it was declared in the :exports. The (defn not-exported [] …​) will not be accessible and will most likely be removed entirely in :advanced release builds.

Module Default Exports

ES Module have this one "special" default export, which you’ll often see used in JS examples. This can be expressed by defining the default exports like any other.

{:source-paths ["src/main"]
 :dev-http {8000 "public"}
 :builds
 {:app
  {:target :esm
   :output-dir "public/js"
   :modules {:demo {:exports {default demo.lib/hello}}}}}}

And the import side changing to

<script type="module">
  import hello from "/js/demo.js";
  hello();
</script>

Many platforms or systems apply special meaning to this default export, but it is declared like any other in the build config.

Module :init-fn

Sometimes you may not require any :exports and instead just want the code to run automatically when the module is loaded. This can be done via :init-fn.

{:source-paths ["src/main"]
 :dev-http {8000 "public"}
 :builds
 {:app
  {:target :esm
   :output-dir "public/js"
   :modules {:demo {:init-fn demo.lib/hello}}}}}

And the HTML

<script type="module" src="js/demo.js"></script>

In can also be combined with :exports to run a function and still provide :exports

{:source-paths ["src/main"]
 :dev-http {8000 "public"}
 :builds
 {:app
  {:target :esm
   :output-dir "public/js"
   :modules
   {:demo
    {:init-fn demo.lib/hello
     :exports {hello demo.lib/hello}}}}}}

Keeping this HTML will essentially just log twice on page load.

<script type="module">
  import hello from "/js/demo.js";
  hello();
</script>

8.2. Module Splitting

{:source-paths ["src/main"]
 :dev-http {8000 "public"}
 :builds
 {:app
  {:target :esm
   :output-dir "public/js"
   :modules
   {:base
    {:entries []}
    :hello
    {:exports {hello demo.lib/hello}
     :depends-on #{:base}}
    :other
    {:exports {foo demo.foo/foo}
     :depends-on #{:base}}
    }}}}

And adding

(ns demo.foo)
(defn foo []
  (js/console.log "foo"))

Here we declare 3 modules with one :base module and two other modules which both depend on the :base module. The :base module declared an empty :entries [] vector which is a convenience to say that it should extract all the namespaces that both of the other modules share (eg. cljs.core in this case).

You may now load each :module independently in the HTML.

<script type="module">
  import hello from "/js/hello.js";
  hello();
</script>

The browser will automatically load the /js/base.js as well, but not the /js/other.js as the code above doesn’t need it. You can use :modules to split code for separate sections of your website for example.

8.3. Dynamic Module Import

Modules can also be loaded dynamically at runtime via the provided shadow.esm/dynamic-import helper.

(ns my.app
  (:require
    [shadow.esm :refer (dynamic-import)]
    [shadow.cljs.modern :refer (js-await)]))
(defn foo []
  (js-await [mod (dynamic-import "https://cdn.pika.dev/preact@^10.0.0")]
    (js/console.log "loaded module" mod)))

This would load an external ESM module dynamically at runtime without it ever being part of the build. You can of course also load your own :modules dynamically this way too.

8.4. Third Party Tool Integration

In the default :runtime :browser setup all dependencies are bundled and provided by shadow-cljs. This is done so the output is directly loadable in the Browser. When importing the :target :esm output into another build tool environment (eg. webpack) that may lead to duplicated dependencies.

Instead, you can configure shadow-cljs to not bundle any JS dependencies and instead leave that to the other tool.

This is done by setting :js-provider in your build config.

{:source-paths ["src/main"]
 :dev-http {8000 "public"}
 :builds
 {:app
  {:target :esm
   :output-dir "public/js"
   :js-options {:js-provider :import}
   :modules {:demo {:exports {default demo.lib/hello}}}}}}

For this build shadow-cljs will only compile and bundle CLJS code, but leave all other JS code to be provided by some other tool later. Note that if you have (:require ["react"]) or any other npm dependency in your build the output from shadow-cljs MUST be processed by another tool first before it becomes loadable in the Browser. Only set this if some other tool is actually going to provide the required dependencies.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文