Evaluating JavaScript code via import()

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  

参考链接