The `import()` operator lets us dynamically load ECMAScript modules. But they can also be used to evaluate JavaScript code (as Andrea Giammarchi recently pointed out to me), as an alternative to `eval()`. This blog post explains how that works.
`eval()` does not support `export` and `import`
A significant limitation of `eval()` is that it doesn’t support module syntax such as `export` and `import`.
If we use `import()` instead of `eval()`, we can actually evaluate module code, as we will see later in this blog post.
In the future, we may get Realms which are, roughly, a more powerful `eval()` with support for modules.
Evaluating simple code via `import()`
Let’s start by evaluating a `console.log()` via `import()`:
const js = `console.log('Hello everyone!');`;
const encodedJs = encodeURIComponent(js);
const dataUri = 'data:text/javascript;charset=utf-8,'
+ encodedJs;
import(dataUri);
// Output:
// 'Hello everyone!'
What is going on here?
- First we create a so-called data URI. The protocol of this kind of URI is `data:`. The remainder of the URI encodes the full resource instead pointing to it. In this case, the data URI contains a complete ECMAScript module – whose content type is `text/javascript`.
- Then we dynamically import this module and therefore execute it.
Warning: This code only works in web browsers. On Node.js, `import()` does not support data URIs.
Accessing an export of an evaluated module
The fulfillment value of the Promise returned by `import()` is a module namespace object. That gives us access to the default export and the named exports of the module. In the following example, we access the default export:
const js = `export default 'Returned value'`;
const dataUri = 'data:text/javascript;charset=utf-8,'
+ encodeURIComponent(js);
import(dataUri)
.then((namespaceObject) => {
assert.equal(namespaceObject.default, 'Returned value');
})
.catch((err) => {
console.log(err.message); // "Importing a module script failed."
// apply some logic, e.g. show a feedback for the user
});
Creating data URIs via tagged templates
With an appropriate function `esm` (whose implementation we’ll see later), we can rewrite the previous example and create the data URI via a tagged template:
const dataUri = esm`export default 'Returned value'`;
import(dataUri)
.then((namespaceObject) => {
assert.equal(namespaceObject.default, 'Returned value');
})
.catch((err) => {
console.log(err.message); // "Importing a module script failed."
// apply some logic, e.g. show a feedback for the user
});
The implementation of `esm` looks as follows:
function esm(templateStrings, ...substitutions) {
let js = templateStrings.raw[0];
for (let i=0; i<substitutions.length; i++) {
js += substitutions[i] + templateStrings.raw[i+1];
}
return 'da
For the encoding, we have switched from `charset=utf-8` to `base64`. Compare:
- Source code: `'a' < 'b'`
- Data URI 1: `data:text/javascript;charset=utf-8,'a'%20%3C%20'b'`
- Data URI 2: `data:text/javascript;base64,J2EnIDwgJ2In`
Each of the two ways of encoding has different pros and cons:
- Benefits of
charset=utf-8
(percent-encoding):
- Much of the source code is still readable.
- Benefits of
base64
:
- The URIs are usually shorter.
- Easier to nest because it doesn’t contain special characters such as apostrophes. We’ll see an example of nesting in the next section.
`btoa()` is a global utility function that encodes a string via base 64. Caveats:
- It is not available on Node.js.
- It should only be used for characters whose Unicode code points range from 0 to 255.
Evaluating a module that imports another module
With tagged templates, we can nest data URIs and encode a module `m2` that imports another module `m1`:
const m1 = esm`export function f() { return 'Hello!' }`;
const m2 = esm`import {f} from '${m1}'; export default f()+f();`;
import(m2)
.then(ns => assert.equal(ns.default, 'Hello!Hello!'))
.catch((err) => {
console.log(err.message); // "Importing a module script failed."
// apply some logic, e.g. show a feedback for the user
});
Further reading
参考链接