es6-Module 的语法
Module 的语法
概述
ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。
ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。
1 | // CommonJS模块 |
上面代码的实质是整体加载 fs 模块(即加载 fs 的所有方法),生成一个对象(_fs),然后再从这个对象上面读取 3 个方法。这种加载称为“运行时加载”,因为只有运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”。
ES6 模块不是对象,而是通过 export 命令显式指定输出的代码,再通过 import 命令输入。
1 | // ES6模块 |
上面代码的实质是从 fs 模块加载 3 个方法,其他方法不加载。这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。当然,这也导致了没法引用 ES6 模块本身,因为它不是对象。
严格模式
ES6 的模块自动采用严格模式,不管你有没有在模块头部加上”use strict”;。
export 命令
export 命令用于规定模块的对外接口
1 | // profile.js |
可以使用 as 关键字重命名。
1 | function v1() { ... } |
export 语句输出的接口,与其对应的值是动态绑定关系。
1 | export var foo = "bar"; |
上面代码输出变量 foo,值为 bar,500 毫秒之后变成 baz。
这一点与 CommonJS 规范完全不同。CommonJS 模块输出的是值的缓存,不存在动态更新
最后,export 命令可以出现在模块的任何位置,只要处于模块顶层就可以。如果处于块级作用域内,就会报错,import 命令也是如此。这是因为处于条件代码块之中,就没法做静态优化了,违背了 ES6 模块的设计初衷。
import 命令
通过 import 命令加载模块。
1 | import { lastName as surname } from "./profile.js"; |
import 命令具有提升效果。
1 | foo(); |
模块的整体加载
1 | import * as circle from "./circle"; |
export default 命令
1 | // 输出 |
1 | // 输入 |
export 与 import 的复合写法
1 | export { foo, bar } from "my_module"; |
模块的继承
假设有一个 circleplus 模块,继承了 circle 模块。
1 | // circleplus.js |
import()
require 是运行时加载模块,import 命令无法取代 require 的动态加载功能。ES2020 提案 引入 import()函数,支持动态加载模块。
import()返回一个 Promise 对象。
1 | const main = document.querySelector("main"); |
import()类似于 Node 的 require 方法,区别主要是前者是异步加载,后者是同步加载。
Module 的加载实现
浏览器加载
<script>
标签打开 defer 或 async 属性,脚本就会异步加载。
defer 与 async 的区别是:defer 要等到整个页面在内存中正常渲染结束(DOM 结构完全生成,以及其他脚本执行完成),才会执行;async 一旦下载完,渲染引擎就会中断渲染,执行这个脚本以后,再继续渲染。一句话,defer 是“渲染完再执行”,async 是“下载完就执行”。另外,如果有多个 defer 脚本,会按照它们在页面出现的顺序加载,而多个 async 脚本是不能保证加载顺序的。
浏览器加载 ES6 模块,也使用<script>
标签,但是要加入 type=”module”属性。
带有 type=”module”的<script>
,都是异步加载,不会造成堵塞浏览器,即等到整个页面渲染完,再执行模块脚本,等同于打开了<script>
标签的 defer 属性。
<script>
标签的 async 属性也可以打开,这时只要加载完成,渲染引擎就会中断渲染立即执行。执行完成后,再恢复渲染。
ES6 模块与 CommonJS 模块的差异
它们有两个重大差异
- CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
- CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
第二个差异是因为 CommonJS 加载的是一个对象(即 module.exports 属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。
第一个差异。CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。
Node.js 加载
概述
Node.js 对 ES6 模块的处理比较麻烦,因为它有自己的 CommonJS 模块格式,与 ES6 模块格式是不兼容的。目前的解决方案是,将两者分开,ES6 模块和 CommonJS 采用各自的加载方案。从 v13.2 版本开始,Node.js 已经默认打开了 ES6 模块支持。
.mjs 文件总是以 ES6 模块加载,.cjs 文件总是以 CommonJS 模块加载,.js 文件的加载取决于 package.json 里面 type 字段的设置。
main 字段
package.json 文件有两个字段可以指定模块的入口文件:main 和 exports。比较简单的模块,可以只使用 main 字段,指定模块加载的入口文件。
1 | // ./node_modules/es-module-package/package.json |
上面代码指定项目的入口脚本为./src/index.js,它的格式为 ES6 模块。如果没有 type 字段,index.js 就会被解释为 CommonJS 模块。
exports 字段
exports 字段的优先级高于 main 字段。它有多种用法。
(1)子目录别名
package.json 文件的 exports 字段可以指定脚本或子目录的别名。
1 | // ./node_modules/es-module-package/package.json |
上面的代码指定 src/submodule.js 别名为 submodule,然后就可以从别名加载这个文件。
(2)main 的别名
exports 字段的别名如果是.,就代表模块的主入口,优先级高于 main 字段,并且可以直接简写成 exports 字段的值。
1 | { |
(3)条件加载
利用.这个别名,可以为 ES6 模块和 CommonJS 指定不同的入口。目前,这个功能需要在 Node.js 运行的时候,打开–experimental-conditional-exports 标志。
1 | { |
ES6 模块加载 CommonJS 模块
目前,一个模块同时支持 ES6 和 CommonJS 两种格式的常见方法是,package.json 文件的 main 字段指定 CommonJS 入口,给 Node.js 使用;module 字段指定 ES6 模块入口,给打包工具使用,因为 Node.js 不认识 module 字段。
有了上一节的条件加载以后,Node.js 本身就可以同时处理两种模块。
1 | // ./node_modules/pkg/package.json |
ES6 模块可以加载这个文件。
1 | // ./node_modules/pkg/wrapper.mjs |
import 命令加载 CommonJS 模块,只能整体加载,不能只加载单一的输出项。
还有一种变通的加载方法,就是使用 Node.js 内置的 module.createRequire()方法。
1 | // cjs.cjs |
CommonJS 模块加载 ES6 模块
CommonJS 的 require 命令不能加载 ES6 模块,会报错,只能使用 import()这个方法加载。
1 | (async () => { |
Node.js 的内置模块
Node.js 的内置模块可以整体加载,也可以加载指定的输出项。
1 | // 整体加载 |
循环加载
CommonJS 模块的循环加载
CommonJS 的一个模块,就是一个脚本文件。require 命令第一次加载该脚本,就会执行整个脚本,然后在内存生成一个对象。
1 | { |
以后需要用到这个模块的时候,就会到 exports 属性上面取值。即使再次执行 require 命令,也不会再次执行该模块,而是到缓存之中取值。
CommonJS 模块的重要特性是加载时执行,即脚本代码在 require 的时候,就会全部执行。一旦出现某个模块被”循环加载”,就只输出已经执行的部分,还未执行的部分不会输出。
ES6 模块的循环加载
ES6 处理“循环加载”与 CommonJS 有本质的不同。ES6 模块是动态引用,如果使用 import 从一个模块加载变量(即 import foo from ‘foo’),那些变量不会被缓存,而是成为一个指向被加载模块的引用,需要开发者自己保证,真正取值的时候能够取到值。
请看下面这个例子。
1 | // a.mjs |
上面代码中,a.mjs 加载 b.mjs,b.mjs 又加载 a.mjs,构成循环加载。执行 a.mjs,结果如下。
1 | $ node --experimental-modules a.mjs |