Most module systems assume access to a fast filesystem. They use blocking I/O. They search many directories looking for files.
Obviously that won’t fly on the Web. We need a module system that works asynchronously.
Things can happen in an asynchronous module system that can’t happen in synchronous systems.
-
A module’s dependencies can load in parallel.
-
While dependencies are loading for one module, the Web page may decide to run some code that imports another module. And those two loading modules may share some dependencies.
These cases have affected the design in a few places.
Many module systems have a single module registry that every part of
the program uses. sys.modules
in Python is an example. But in a
browser, each window
must have its own module registry, to prevent Web
pages from tampering with one another or gaining access to one
another’s global object and prototype objects.
Most module systems only have implementations, not full specifications. The precise behavior of the ES6 module system must be specified in some detail, to minimize cross-browser compatibility gotchas.
The module system consists of:
-
an HTML
<module>
element; -
some new declarative JS syntax (
import
,export
, andmodule
keywords); and -
a new
Loader
builtin constructor with an asynchronous API, for loading modules dynamically and for customizing module loading.
It will look like this:
module _ from "underscore";
...followed by code that uses _
.
The HTML syntax isn’t completely worked out yet, but we anticipate something like:
<module>
module _ from "underscore";
// ... code that uses _ ...
</module>
How it works:
The <module>
element is like <script>
, but asynchronous. Code in a
<module>
runs as soon as the HTML page and any imported modules have
finished loading.
By default, we expect the module loader will load "underscore"
from
the relative URL underscore.js
; this is configurable.
The import
keyword can’t be used in a <script>
. Rationale:
The system is asynchronous. The import
keyword never blocks the
rest of the page from loading and staying responsive.
(If a <script>
or eval()
code needs to load a module, it can, using
the asynchronous Loader API, which we’ll get to later.)
Simple:
module Backbone from "backbone";
If "backbone"
contains this:
module _ from "underscore";
then we load "underscore"
. No code runs until all dependencies are
loaded.
You do not have to change any JS code. In particular, your import
declarations, which will likely be scattered across all your JS files,
do not need to change.
Use case: importing minimized modularized jquery (a package with many modules in one file)
Use case: importing the same modules from many different web pages on a site
Use case: importing a multifile package in source form
Use case: bundling your package for others to use
Recall an earlier example:
module Backbone from "backbone";
where the "backbone"
module contains this:
module _ from "underscore";
Note that this example, as written, causes two network round-trips in
series. First we load the "backbone"
module. When that loads, the
module system sees that Backbone requires Underscore, so we then load
"underscore"
.
The system can’t start loading "underscore"
until the source
code for "backbone"
arrives; it simply has no way of knowing what
"backbone"
imports until it can look at the source code.
Can the extra network round-trip be eliminated? Yes. Here are some ways:
-
Use HTTP2/SPDY. The server will figure out that it should send both files (Backbone and Underscore) whenever a client asks for Backbone. This is the nicest approach. It requires no changes to your JS or HTML code.
-
Kick off loads eagerly. You can change your JS code to say
module _ from "underscore"; module Backbone from "backbone";
or add this to your HTML code:
<module name="underscore" src="..."></module>
Either way, the point is to kick off all loads as early as possible, so that they can run in parallel rather than in series, reducing the total wait time.
-
Make a bundle. You can bundle several modules into a single file on the server, and send that to the client eagerly, cutting short a bunch of round trips. Loader hooks make this easy. We'll see an example below.
-
Use a new kind of URL? W3C is considering a "zip URL" feature that would be another very easy-to-deploy alternative.
But if your load times are satisfactory for now, you don’t need to do anything. The system is designed so that all these techniques are easy to deploy after the fact, without significant changes to your application code.
Use case: moving your JS to a CDN
Use case: deploying all/several modules in a single file
Use case: updating to a newer version of a third-party package
Use case: imports within a package
Use case: loading multiple versions of a module
Use case: reusing npm/AMD modules/packages
Use case: how node might integrate import
with npm
Use case: using the same source code in node and the browser
Use case: automatically translating coffeescript source files
Use case: implementing a direct-eval-like feature in terms of JS direct eval
Use case: running code in a restricted environment. This has several parts.
- isolated globals
- completely controlling what
import
can expose - avoiding object leaks