ES6 modules
are the crown of the
ECMAScript 2015 specification.
Together with
block-scope (const
, let
)
they provide the only real
encapsulation-mechanism
of that language version.
Modules let us control the dependencies of our scripts (import), and what is visible of them (export).
Modules are →
- → file-based;
a module imports another one by stating a path relative to its own file-system location (slash-separated);
one file.js
is one script is one module - →
singletons by nature, you can not instantiate them;
the browser loads a module just once per page (tab, window);
thinking in Java, all fields and functions inside a module would bestatic
- → an encapsulation-mechanism;
other modules can not see fields and functions that have not been exported - → read-only views of exported fields (variables, constants);
you can see an exported field of moduleA
in moduleB
when you imported it, but you can not change its value
Don't confuse modules with classes. Modules provide control for deployment and dependency-loading, classes provide state when instantiated to objects. Modules are meant to contain classes, or be factories for objects of classes.
ES6 modules are already available in HTML5 browsers. This article describes how you can create a test-environment (without node-js!) for studying them.
Make Modules Work in Browsers
For a deeper insight
(CORS,
loading order of script
elements)
read the article
"ECMAScript modules in browsers".
- When Firefox, or lower than Safari 10.1, Chrome 61 or Edge 16, configure browser to support modules
- Firefox: enter address
about:config
, switchdom.moduleScripts.enabled
to true; Firefox also runs fine withfile:/
protocol (loading page from local disk) - Chrome refuses to load modules when using the
file:/
protocol ("blocked by CORS", "strict MIME type checking"), so you need to deploy your page-resources to some HTTP server)
- Firefox: enter address
- Set attribute type = "module" in all HTML
script
elements that import modules
This works for both the newest desktop- and mobile-browsers.
Example Sources
Here are the files and directories used in the following.
The file es6-modules.html is the test page.
It imports es6-modules.js in a <script "type = module">
element.
The file es6-modules.js is a module, and it imports all other *.js files listed here,
that means it depends on them.
modules
io
math
You can try out an example similar to this on my homepage (unfortunately I can not deploy modules on this Blog server:-).
HTML
es6-modules.html
Following is HTML using modules. It defines two buttons demonstrating different ways to integrate module functions:
- "Page-driven module call"
When you want to call a module-function directly from a button callback, you will be confronted with the separation between module scripts and traditional scripts. It is not possible to import modules into a normal script. Thus you need to write a<script "type = module">
element that imports the required modules and sets the needed functions onto some global page-object that is reachable from callbacks, as done inconst page = {}
andpage.foobar = foobar
. Mind that you can not create theconst page = {}
in a<script "type = module">
element, because then it would be a local constant! - "Module-driven module call"
The other way is to write an adapter<script "type = module">
that imports the required modules and installs all callbacks itself, as done indocument.getElementById("foobarTestButton").addEventListener("click", foobar)
. This is most likely the better way to go.
<div> <button onclick="page.foobar()">Page-driven module call</button> <button id="foobarTestButton">Module-driven module call</button> </div> <fieldset> <legend>Success Messages</legend> <p id="successMessages"></p> </fieldset> <fieldset> <legend>Error Messages</legend> <p id="errorMessages"></p> </fieldset> <script> const page = {} </script> <script type="module"> import foobar from "./es6-modules.js" page.foobar = foobar </script> <script type="module"> import foobar from "./es6-modules.js" const foobarTestButton = document.getElementById("foobarTestButton") foobarTestButton.addEventListener("click", foobar) </script> <script type="module"> import { configure } from "./modules/io/output.js" configure("successMessages", "errorMessages") </script>
Two buttons are created here.
The first one has a programmed callback that calls page.foobar()
.
I named this "Page-driven".
The second button has an id, but no callback.
The callback is installed in a script
element below.
I named this "Module-driven".
Next there are two output-areas "Success Messages" and "Error Messages".
They will get configured to be the targets for output.js
module.
The subsequent script
elements do what is required
to use module functions in a page-driven and a module-driven way.
The first one is a non-module script, it establishes the global page
object.
The next one imports a module and sets the foobar
module-function
into the page
object, to be available for the first button.
The following script
installs a module-function as callback into the second button.
The last script
element finally configures the output.js
module to write to the
output-areas with ids "successMessages" and "errorMessages".
Mind that currently import-paths must be written as from "./es6-modules.js"
,
giving a bare from "es6-modules.js"
would fail.
It is not a problem to import a module several times in different script-elements,
the browser would load each module just once.
Library Modules
counter.js
This is a stateful module that holds a counter, and a function that can increment it.
The counter
field state itself can not be changed from outside (although exported),
but is visible (because exported).
To change it, modules must call the increment()
function.
There is no way to reset the counter except a browser page reload.
export let counter = 0 export function increment() { counter++; }
cycle*.js
These are two modules demonstrating the ES6-ability to resolve cyclic dependencies.
Module cycle-1.js
defines foo()
calling bar()
in cycle-2.js
, that
again calls foo()
. If there weren't preventions against recursion by holding a private state,
these two functions would actually call each other in an endless loop!
cycle-1.js
import { bar } from "./cycle-2.js" let state = "idle" export function foo() { if (state === "running") return /* avoid recursion */ state = "running" try { bar() } finally { state = "idle" } }
cycle-2.js
import { foo } from "./cycle-1.js" let running = false export function bar() { if (running) return /* avoid recursion */ running = true try { foo() } finally { running = false } }
output.js
A stateful module that contains functions for appending success- and error-messages to specified elements.
That means the module must be parameterized with the according element-ids,
which can be done by calling configure(newSuccessElementId, newErrorElementId)
.
See the last script
element in es6-modules.html which does this.
let successElementId = "success" let errorElementId = "error" function message(text, elementId) { const outputElement = document.getElementById(elementId) outputElement.innerHTML += ( text+"<br>" ) } export function configure(newSuccessElementId, newErrorElementId) { if (newSuccessElementId) successElementId = newSuccessElementId if (newErrorElementId) errorElementId = newErrorElementId } export function success(text) { message(text, successElementId) } export function error(text) { message(text, errorElementId) }
mathematics.js
This is an example for a stateless module that contains lots of functions around a specific topic like mathematics.
export function add(first, second) { return first + second } export function multiply(first, second) { return first * second }
Page Module
es6-modules.js
Imports and uses all other modules.
Its foobar()
function performs various tests with them.
You can launch it by clicking on one of the buttons in the HTML page.
Outputs will go to the configured areas.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | import { success, error } from "./modules/io/output.js" import { counter, increment } from "./modules/counter.js" import * as mathematics from "./modules/math/mathematics.js" import { foo as cycleTest } from "./modules/cycle-1.js" export default function foobar() { /* use module "counter" */ try { counter = 99 /* module fields are readonly! */ } catch (errorMessage) { error(errorMessage) } try { const oldValue = counter increment() if (oldValue + 1 !== counter) error("counter didn't increment: "+counter) else success("counter after increment: "+counter) } catch (errorMessage) { error(errorMessage) } /* use module "mathematics" */ const oneAndTwo = mathematics.add(1, 2) if (oneAndTwo !== 3) error("1 + 2 is not 3: "+oneAndTwo) else success("add() worked: 1 + 2 == 3") const twoThreeTimes = mathematics.multiply(2, 3) if (twoThreeTimes !== 6) error("2 * 3 is not 6: "+twoThreeTimes) else success("multiply() worked: 2 * 3 == 6") /* use cyclic modules */ try { cycleTest() success("foo() is available despite cyclic dependency") } catch (errorMessage) { error(errorMessage) } } |
The import { success, error } from "./modules/io/output.js"
imports just the needed functions.
The configure()
call already has been done in es6-modules.html,
and only there the ids of the message-elements should be duplicated.
We import { counter, increment } from "./modules/counter.js"
to also see the field counter
.
Subsequently writing to that field gets tested, this results in an exception (→ fields are exported as read-only views!).
The module mathematics.js
is an example for a collection of many functions around mathematics,
and let's assume that we also need to use most of them.
Thus mathematics.js
is imported using the asterisk-wildcard,
to be seen in in import * as mathematics from "modules/math/mathematics.js"
.
Mind that the "as alias" is required when importing that way!
Finally one of the cyclic modules gets imported.
I rename the foo
function to cycleTest
using an alias.
When no exception gets thrown on calling cycleTest()
, the dependency-cycle was resolved correctly by ES6.
The export default function foobar()
statement exports the function foobar()
in a way
that it can be imported without { destructuring }, to be seen in es6-modules.html.
Mind that just one default
export is possible per module!
Further it is not possible to use export default
together with
var
, let
or const
.
The code of foobar()
performs various tests on the imported module fields and functions.
It should be easy to see what it does, so I won't explain it in further detail.
Resume
We should keep in mind that modules can hold state, but that state would be the same for all
components inside the HTML page. As soon as you implement a TABLE
module, and
you have more than one TABLE
instances in your page that use this module,
they would compete for the state (singleton!).
Remember that static
methods in Java never should refer to any field other than its parameters.
ES6 modules have to be regarded as "static" implementations (in the sense of Java),
and you should use them as factories for class-instances that represent state-holding objects,
and only in that class all functionality should be implemented.
(Thinking further, that everything in classes is public, i.e. no encapsulation at all,
what remains from the beautiful encapsulation-mechanism of modules,
non-exported fields and functions not being visible? Nothing.)
It took me much more time to make this example work in Firefox and Chrome than writing it.
I had to experiment a lot with import
statements and file extensions.
Finding out that absolute paths are supported by ES6,
but are absolute to the file-system root (what else?),
was a little frustrating.
Why would we need such? It binds ES6 projects to the file system of a certain machine!
We are hardcoding the directory-structure of our library also with relative path imports,
and this is hard enough to maintain (imagine that directory structure once changes!).
The third import alternative (to relative and absolute paths) is "module specifiers".
But not even the
specification
states how this should be organized.
Topic of my next Blog will be: which of the (too) many provided export/import possibilities actually make sense?