### 1. 加载规则
浏览器加载 ES6 模块,也使用`<script>`标签,但是要加入`type="module"`属性。
~~~
<script type="module" src="./foo.js"></script>
~~~
上面代码在网页中插入一个模块`foo.js`,由于`type`属性设为`module`,所以浏览器知道这是一个 ES6 模块。
浏览器对于带有`type="module"`的`<script>`,都是异步加载,不会造成堵塞浏览器,即等到整个页面渲染完,再执行模块脚本,等同于打开了`<script>`标签的`defer`属性。
~~~
<script type="module" src="./foo.js"></script>
<!-- 等同于 -->
<script type="module" src="./foo.js" defer></script>
~~~
如果网页有多个`<script type="module">`,它们会按照在页面出现的顺序依次执行。
`<script>`标签的`async`属性也可以打开,这时只要加载完成,渲染引擎就会中断渲染立即执行。执行完成后,再恢复渲染。
~~~
<script type="module" src="./foo.js" async></script>
~~~
一旦使用了`async`属性,`<script type="module">`就不会按照在页面出现的顺序执行,而是只要该模块加载完成,就执行该模块。
ES6 模块也允许内嵌在网页中,语法行为与加载外部脚本完全一致。
~~~
<script type="module">
import utils from "./utils.js";
// other code
</script>
~~~
举例来说,jQuery 就支持模块加载。
~~~
<script type="module">
import $ from "./jquery/src/jquery.js";
$('#message').text('Hi from jQuery!');
</script>
~~~
* 模块之中,顶层的`this`关键字返回`undefined`,而不是指向`window`。也就是说,在模块顶层使用`this`关键字,是无意义的。
### 2. ES6模块与CommonJS中模块的差异
它们有两个重大差异。
* CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
* CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
第二个差异是因为 CommonJS 加载的是一个对象(即`module.exports`属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。
下面重点解释第一个差异。
CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。请看下面这个模块文件`lib.js`的例子。
~~~javascript
// lib.js
var counter = 3;
function incCounter() {
counter++;
}
module.exports = {
counter: counter,
incCounter: incCounter,
};
~~~
上面代码输出内部变量`counter`和改写这个变量的内部方法`incCounter`。然后,在`main.js`里面加载这个模块。
~~~javascript
// main.js
var mod = require('./lib');
console.log(mod.counter); // 3
mod.incCounter();
console.log(mod.counter); // 3
~~~
上面代码说明,`lib.js`模块加载以后,它的内部变化就影响不到输出的`mod.counter`了。这是因为`mod.counter`是一个原始类型的值,会被缓存。除非写成一个函数,才能得到内部变动后的值。
~~~javascript
// lib.js
var counter = 3;
function incCounter() {
counter++;
}
module.exports = {
get counter() {
return counter
},
incCounter: incCounter,
};
~~~
上面代码中,输出的`counter`属性实际上是一个取值器函数。现在再执行`main.js`,就可以正确读取内部变量`counter`的变动了。
~~~bash
$ node main.js
3
4
~~~
ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令`import`,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6 的`import`有点像 Unix 系统的“符号连接”,原始值变了,`import`加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。
还是举上面的例子。
~~~javascript
// lib.js
export let counter = 3;
export function incCounter() {
counter++;
}
// main.js
import { counter, incCounter } from './lib';
console.log(counter); // 3
incCounter();
console.log(counter); // 4
~~~
上面代码说明,ES6 模块输入的变量`counter`是活的,完全反应其所在模块`lib.js`内部的变化。
### 3. Node加载
Node 对 ES6 模块的处理比较麻烦,因为它有自己的 CommonJS 模块格式,与 ES6 模块格式是不兼容的。目前的解决方案是,将两者分开,ES6 模块和 CommonJS 采用各自的加载方案。
Node 要求 ES6 模块采用`.mjs`后缀文件名。也就是说,只要脚本文件里面使用`import`或者`export`命令,那么就必须采用`.mjs`后缀名。`require`命令不能加载`.mjs`文件,会报错,只有`import`命令才可以加载`.mjs`文件。反过来,`.mjs`文件里面也不能使用`require`命令,必须使用`import`。
目前,这项功能还在试验阶段。安装 Node v8.5.0 或以上版本,要用`--experimental-modules`参数才能打开该功能。
~~~bash
$ node --experimental-modules my-app.mjs
~~~
为了与浏览器的`import`加载规则相同,Node 的`.mjs`文件支持 URL 路径。
~~~javascript
import './foo?query=1'; // 加载 ./foo 传入参数 ?query=1
~~~
上面代码中,脚本路径带有参数`?query=1`,Node 会按 URL 规则解读。同一个脚本只要参数不同,就会被加载多次,并且保存成不同的缓存。由于这个原因,只要文件名中含有`:`、`%`、`#`、`?`等特殊字符,最好对这些字符进行转义。
目前,Node 的`import`命令只支持加载本地模块(`file:`协议),不支持加载远程模块。
如果模块名不含路径,那么`import`命令会去`node_modules`目录寻找这个模块。
~~~javascript
import 'baz';
import 'abc/123';
~~~
如果模块名包含路径,那么`import`命令会按照路径去寻找这个名字的脚本文件。
~~~javascript
import 'file:///etc/config/app.json';
import './foo';
import './foo?search';
import '../bar';
import '/baz';
~~~
如果脚本文件省略了后缀名,比如`import './foo'`,Node 会依次尝试四个后缀名:`./foo.mjs`、`./foo.js`、`./foo.json`、`./foo.node`。如果这些脚本文件都不存在,Node 就会去加载`./foo/package.json`的`main`字段指定的脚本。如果`./foo/package.json`不存在或者没有`main`字段,那么就会依次加载`./foo/index.mjs`、`./foo/index.js`、`./foo/index.json`、`./foo/index.node`。如果以上四个文件还是都不存在,就会抛出错误。
最后,Node 的`import`命令是异步加载,这一点与浏览器的处理方法相同。