### 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`命令是異步加載,這一點與瀏覽器的處理方法相同。