使用 import() 操作符,我们可以动态加载 ECMAScript 模块。但是 import() 的应用不仅于此,它还可以作为 eval() 的替代品,用来执行 JavaScript 代码(这一点是最近 Andrea Giammarchi 向我指出的)。这篇博客将会解释这是如何实现的。
eval() 不支持 export 和 import
eval() 的一大缺陷是:它不支持例如 export 和 import 这样的模块语法。
但是如果放弃 eval() 而改为使用 import(),我们就可以执行带有模块的代码,在后文你将能看到这是如何实现的。
未来,我们也许可以使用 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);
// 输出:
// 'Hello everyone!'
这段代码执行后发生了什么?
-
首先,我们创建了所谓的 数据 URI。这种类型的 URI 协议是 data:。URI 的剩余部分中包含了所有资源的编码,而不是指向资源本身的地址。这样,数据 URI 就包含了一个完整的 ECMAScript 模块 —— 它的 content 类型是 text/javascript。
-
然后我们动态引入模块,于是代码被执行。
注意:这段代码只能在浏览器中运行。在 Node.js 环境中,import() 不支持数据 URI。
获取被执行模块的导出
使用一个适当的方法 esm(后文我们会看到该方法是如何实现的),我们可以重写上文的例子,并通过一个标记模版创建数据 URI:
const dataUri = esm`export default 'Returned value'`;
import(dataUri)
.then((namespaceObject) => {
assert.equal(namespaceObject.default, 'Returned value');
});
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-8 切换为 base64,它们两者的对比如下:
- 源代码:‘a’ < ‘b’
- 第一个数据 URI:data:text/javascript;charset=utf-8,‘a’%20%3C%20’b’
- 第二个数据 URI:data:text/javascript;base64,J2EnIDwgJ2In
每种编码方式都各有利弊:
- charset=utf-8(又称百分号编码)的优势: 大部分源码仍具有可读性。
- base64 的优势:URI 更精短。更易嵌套(后文我们会看到),因为它不包含任何如撇号这样的特殊字符。
btoa() 是一个用来将字符串编码为 base 64 代码的全局工具函数。注意:
- 在 Node.js 环境下不可用。
- 仅对码点值在 0 至 255 范围内的 Unicode 字符有效。
执行引用了其他模块的模块
通过标记模版,我们可以嵌套数据 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 => assert.equal(ns.default, 'Hello!Hello!'));