通过 import() 获取 JavaScript 代码值

import() 操作符让我们动态加载地 ECMAScript 模块,但是它们也可以取得 JavaScript 代码运行值(正像 Andrea Giammarchi 向我指出的那样),作为 eval() 的替代。这篇博客文章解释了它是如何工作的。

eval() 不支持 export 和 import

eval() 一个重要限制是它不支持诸如 exportimport 的模块语法。

如果我们使用 import() 替换 eval(),我们可以竟能模块代码取值,比如在这篇博客文章接下来将看到的那样。

在未来,我们能获取 Realms 那样的功能,一个功能更强大支持模块的 eval()

通过 import() 获取简单代码值

让我们开始使用 import() 获取一条 console.log() 输出值:

const js = `console.log('Hello everyone!');`;
const encodedJs = encodeURIComponent(js);
const dataUri = 'data:text/javascript;charset=utf-8,' + encodedJs;
import(dataUri);

// Output:
// 'Hello everyone!'

这里发生了什么?

  • 首先我们创建了所谓的 data URI。这种 URI 的协议是 data:URI 的其余部分编码完整的资源,而不是指向它。 在这种情况下,数据 URI 包含完整的 ECMAScript 模块 - 其内容的类型为 text/javascript
  • 然后,我们动态导入此模块并执行它。

注意:这些代码仅仅工作在网络浏览器。在 Node.js 上,import() 不支持 data URI。

访问一个可求值模块的导出(export)

通过 import() 取得的 Promise 返回的履行值是一个模块命名空间对象。这能让我们访问模块的默认导出(default export) 和名称导出(exports)。在下面的例子中,我们访问了默认导出:

const js = `export default 'Returned value'`;
const dataUri = 'data:text/javascript;charset=utf-8,'
                 + encodeURIComponent(js);
import(dataUri).then((namespaceObject) => {
  console.log(namespaceObject.default === 'Returned value');
  // -> true
});

通过标记模板创建 data URI

通过适用函数 esm(我们将稍后看到其实现),我们可以上面的例子并通过标记模板创建 data URI:

const dataUri = esm`export default 'Returned value'`;
import(dataUri).then((namespaceObject) => {
  console.log(namespaceObject.default === 'Returned value');
  // -> true
});

esm 的实现是这样的:

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 'data:text/javascript;base64,' + btoa(js);
}

对于编码,我们切换 charset=utf-8base64。之间的对比:

  • 源代码:'a' < 'b'
  • Data URI 1: data:text/javascript;charset=utf-8,'a'%20%3C%20'b'
  • Data URI 2: data:text/javascript;base64,J2EnIDwgJ2In

两种编码中的每一种都有不同的利弊:

  • charset = utf-8(百分比编码)的优点:
    • 许多源代码仍然可读。
  • base64 的优点:
    • URI 通常较短。
    • 因为它不包含撇号之类的特殊字符,所以嵌套起来更容易(我们将在后面看到)。

btoa() 是一个使用 base64 编码字符串的全局工具函数。注意:

  • 在 Node.js 上不可用。
  • 仅仅可用于 0 到 255 范围内的 Unicode 字符。

获取一个包含导入其它模块的模块的值

通过标记模板,我们能嵌套 data URI 并对导入另一个模块 m1 的模块 m2 进行编码:

const m1 = esm`export function f() { return 'Hello!' }`;
const m2 = esm`import {f} from '${m1}'; export default f()+f();`;
import(m2).then((ns) => console.log(ns.default === 'Hello!Hello!'));
// -> true

延伸阅读


英文原文:https://2ality.com/2019/10/eval-via-import.html

Copyright © 2019 by Wu WenJun