esm 是什么?
esm 是将 javascript 程序拆分成多个独自模块,并能 按需导入 的规范。和 webpack,babel 不同的是,esm 是 javascript 的规范性能,在浏览器端和 nodejs 中都已失去实现 。应用 esm 的益处是 浏览器能够最优化加载模块,比应用库更有效率。
esm 规范通过 import, export 语法实现模块变量的导入和导出。
esm 模块的特点
- 存在模块作用域,顶层变量都定义在该作用域,内部不可见;
- 模块脚本主动采纳严格模式;
- 模块顶层的 this 关键字返回 undefined;
- esm 是编译时加载,也就是只有所有 import 的模块都加载实现,才会开始执行;
- 同一个模块如果加载屡次,只会执行一次。
export
export 语句用来导出模块中的变量。
// 导出变量
export let count = 1;
export const CONST_VAR = 'CONST_VAR';
// 导出函数
export function incCount() {count += 1;}
// 导出类
export class Demo {
}
function add(x) {return x + count;}
// 应用 export 导出一组变量
export {
count,
add,
// 应用 as 重命名导出的变量
add as addCount,
}
// 导出 default
export default add
// 合并导出其余模块的变量
export {name} from './esm_module2.js'
export * from './esm_module2.js'
import
import 语句用来导入其余模块的变量
// 导入变量
import {count, incCount, CONST_VAR} from './esm_module1.js';
// 通过 as 重命名导入的变量
import {addCount as renamedAddCount} from './esm_module1.js';
// 导入默认
import {default as defaultAdd} from './esm_module1.js';
import add from './esm_module1.js';
// 创立模块对象
import * as module1 from './esm_module1.js';
export 导出的是值援用
esm 模块和 commonjs 模块的一个显著差别是,cjs 导出的是值得拷贝,esm 导出的是值的援用。当模块外部的值被批改时,cjs 获取不到被批改后的值,esm 能够获取到被批改后的值。
cjs 例子
// cjs_module1.js
var count = 1;
function incCount() {count += 1;}
module.exports = {
count: count,
incCount: incCount,
}
// cjs_demo.js
var {count, incCount} = require('./cjs_module1.js');
console.log(count); // 1
incCount();
console.log(count); // 1
esm 例子
// esm_module1.js
let count = 1;
function incCount() {count += 1;}
export {
count,
incCount,
}
// esm_demo.js
import {count, incCount} from './esm_module1.js';
console.log(count); // 1
incCount();
console.log(count); // 2
从实现原理上来看,cjs 的 module.exports 是一个对象,在运行期注入模块。在导出语句 module.exports.count = count 执行时,是给这个对象调配一个 count 的键,并赋值为 1。这之后模块中的 count 变量再怎么变动,都不会烦扰到 module.exports.count
esm 中的 export {count}是导出了 count 变量的一个只读援用,等于说使用者读取 count 时,值的指向还是模块中 count 变量的值。
能够看阮一峰的这篇文章:ES6 入门教程
在 html 中应用 esm
应用 script 标签引入 esm 文件,同时设置 type=module,标识这个模块为顶级模块。浏览器将 esm 文件视为模块文件,辨认模块的 import 语句并加载。
<script src="./esm_main.js" type="module"></script>
如果不设置 type=module,浏览器认为该文件为一般脚本。查看到文件中存在 import 语句时,会报如下谬误:
esm 的加载机制
esm 规范没有规定模块的加载细节,将这些留给具体环境实现。大抵上分为上面四个步骤:
解析:实现读取模块的源代码并查看语法错误;
加载:递归加载所有 import 的模块;
链接:对每个加载的模块,都生成一个模块作用域,该模块下的所有全局申明都绑定到该作用域上,包含从其余模块导入的内容;
运行时:实现所有 import 的加载和链接,脚本运行每个曾经加载的模块中的语句。当运行到全局申明时,什么也不会做(在链接阶段曾经将申明绑定到模块作用域)。
能够看下 mdn 上的这篇深刻 esm 的文章:ES6 In Depth: Modules
动静加载模块
esm 的一个重要个性是编译时加载,这有利于引擎的动态剖析。加载的过程会先于代码的执行。却也导致 import 导入语句不能在函数或者 if 语句中执行:
// 报语法错误
if (true) {import add from './esm_module1.js';}
es2020 提案引入 import()函数,用来动静加载模块,并且能够用在函数和 if 语句中。
import('./esm_module1.js')
.then(module => {console.log(module);
})
import()函数承受要加载的模块相对路径,返回一个 Promise 对象,内容是要加载的模块对象。
应用 import()函数还能够实现依据变量动静加载模块
async function getTemplate(templateName) {let template = await import(`./templates/${templateName}`);
console.log(template);
}
getTemplate("foo");
getTemplate("bar");
getTemplate("baz");