Module 的加載實現

上一章介紹了模塊的語法,本章介紹如何在瀏覽器和 Node 之中加載 ES6 模塊,以及實際開發中常常遇到的一些問題(好比循環加載)。javascript

瀏覽器加載

傳統方法

HTML 網頁中,瀏覽器經過<script>標籤加載 JavaScript 腳本。html

<!-- 頁面內嵌的腳本 -->
<script type="application/javascript"> // module code </script>

<!-- 外部腳本 -->
<script type="application/javascript" src="path/to/myModule.js"> </script>

上面代碼中,因爲瀏覽器腳本的默認語言是 JavaScript,所以type="application/javascript"能夠省略。java

默認狀況下,瀏覽器是同步加載 JavaScript 腳本,即渲染引擎遇到<script>標籤就會停下來,等到執行完腳本,再繼續向下渲染。若是是外部腳本,還必須加入腳本下載的時間。node

若是腳本體積很大,下載和執行的時間就會很長,所以形成瀏覽器堵塞,用戶會感受到瀏覽器「卡死」了,沒有任何響應。這顯然是很很差的體驗,因此瀏覽器容許腳本異步加載,下面就是兩種異步加載的語法。git

<script src="path/to/myModule.js" defer></script>
<script src="path/to/myModule.js" async></script>

上面代碼中,<script>標籤打開deferasync屬性,腳本就會異步加載。渲染引擎遇到這一行命令,就會開始下載外部腳本,但不會等它下載和執行,而是直接執行後面的命令。es6

deferasync的區別是:defer要等到整個頁面在內存中正常渲染結束(DOM 結構徹底生成,以及其餘腳本執行完成),纔會執行;async一旦下載完,渲染引擎就會中斷渲染,執行這個腳本之後,再繼續渲染。一句話,defer是「渲染完再執行」,async是「下載完就執行」。另外,若是有多個defer腳本,會按照它們在頁面出現的順序加載,而多個async腳本是不能保證加載順序的。github

加載規則

瀏覽器加載 ES6 模塊,也使用<script>標籤,可是要加入type="module"屬性。express

<script type="module" src="./foo.js"></script>

上面代碼在網頁中插入一個模塊foo.js,因爲type屬性設爲module,因此瀏覽器知道這是一個 ES6 模塊。npm

瀏覽器對於帶有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>

對於外部的模塊腳本(上例是foo.js),有幾點須要注意。

  • 代碼是在模塊做用域之中運行,而不是在全局做用域運行。模塊內部的頂層變量,外部不可見。
  • 模塊腳本自動採用嚴格模式,無論有沒有聲明use strict
  • 模塊之中,可使用import命令加載其餘模塊(.js後綴不可省略,須要提供絕對 URL 或相對 URL),也可使用export命令輸出對外接口。
  • 模塊之中,頂層的this關鍵字返回undefined,而不是指向window。也就是說,在模塊頂層使用this關鍵字,是無心義的。
  • 同一個模塊若是加載屢次,將只執行一次。

下面是一個示例模塊。

import utils from 'https://example.com/js/utils.js';

const x = 1;

console.log(x === window.x); //false
console.log(this === undefined); // true

delete x; // 句法錯誤,嚴格模式禁止刪除變量

利用頂層的this等於undefined這個語法點,能夠偵測當前代碼是否在 ES6 模塊之中。

const isNotModuleScript = this !== undefined;

ES6 模塊與 CommonJS 模塊的差別

討論 Node 加載 ES6 模塊以前,必須瞭解 ES6 模塊與 CommonJS 模塊徹底不一樣。

它們有兩個重大差別。

  • CommonJS 模塊輸出的是一個值的拷貝,ES6 模塊輸出的是值的引用。
  • CommonJS 模塊是運行時加載,ES6 模塊是編譯時輸出接口。

第二個差別是由於 CommonJS 加載的是一個對象(即module.exports屬性),該對象只有在腳本運行完纔會生成。而 ES6 模塊不是對象,它的對外接口只是一種靜態定義,在代碼靜態解析階段就會生成。

下面重點解釋第一個差別。

CommonJS 模塊輸出的是值的拷貝,也就是說,一旦輸出一個值,模塊內部的變化就影響不到這個值。請看下面這個模塊文件lib.js的例子。

// lib.js
var counter = 3;
function incCounter() {
  counter++;
}
module.exports = {
  counter: counter,
  incCounter: incCounter,
};

上面代碼輸出內部變量counter和改寫這個變量的內部方法incCounter。而後,在main.js裏面加載這個模塊。

// main.js
var mod = require('./lib');

console.log(mod.counter);  // 3
mod.incCounter();
console.log(mod.counter); // 3

上面代碼說明,lib.js模塊加載之後,它的內部變化就影響不到輸出的mod.counter了。這是由於mod.counter是一個原始類型的值,會被緩存。除非寫成一個函數,才能獲得內部變更後的值。

// lib.js
var counter = 3;
function incCounter() {
  counter++;
}
module.exports = {
  get counter() {
    return counter
  },
  incCounter: incCounter,
};

上面代碼中,輸出的counter屬性其實是一個取值器函數。如今再執行main.js,就能夠正確讀取內部變量counter的變更了。

$ node main.js
3
4

ES6 模塊的運行機制與 CommonJS 不同。JS 引擎對腳本靜態分析的時候,遇到模塊加載命令import,就會生成一個只讀引用。等到腳本真正執行時,再根據這個只讀引用,到被加載的那個模塊裏面去取值。換句話說,ES6 的import有點像 Unix 系統的「符號鏈接」,原始值變了,import加載的值也會跟着變。所以,ES6 模塊是動態引用,而且不會緩存值,模塊裏面的變量綁定其所在的模塊。

仍是舉上面的例子。

// 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內部的變化。

再舉一個出如今export一節中的例子。

// m1.js
export var foo = 'bar';
setTimeout(() => foo = 'baz', 500);

// m2.js
import {foo} from './m1.js';
console.log(foo);
setTimeout(() => console.log(foo), 500);

上面代碼中,m1.js的變量foo,在剛加載時等於bar,過了 500 毫秒,又變爲等於baz

讓咱們看看,m2.js可否正確讀取這個變化。

$ babel-node m2.js

bar
baz

上面代碼代表,ES6 模塊不會緩存運行結果,而是動態地去被加載的模塊取值,而且變量老是綁定其所在的模塊。

因爲 ES6 輸入的模塊變量,只是一個「符號鏈接」,因此這個變量是隻讀的,對它進行從新賦值會報錯。

// lib.js
export let obj = {};

// main.js
import { obj } from './lib';

obj.prop = 123; // OK
obj = {}; // TypeError

上面代碼中,main.jslib.js輸入變量obj,能夠對obj添加屬性,可是從新賦值就會報錯。由於變量obj指向的地址是隻讀的,不能從新賦值,這就比如main.js創造了一個名爲objconst變量。

最後,export經過接口,輸出的是同一個值。不一樣的腳本加載這個接口,獲得的都是一樣的實例。

// mod.js
function C() {
  this.sum = 0;
  this.add = function () {
    this.sum += 1;
  };
  this.show = function () {
    console.log(this.sum);
  };
}

export let c = new C();

上面的腳本mod.js,輸出的是一個C的實例。不一樣的腳本加載這個模塊,獲得的都是同一個實例。

// x.js
import {c} from './mod';
c.add();

// y.js
import {c} from './mod';
c.show();

// main.js
import './x';
import './y';

如今執行main.js,輸出的是1

$ babel-node main.js
1

這就證實了x.jsy.js加載的都是C的同一個實例。

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參數才能打開該功能。

$ node --experimental-modules my-app.mjs

爲了與瀏覽器的import加載規則相同,Node 的.mjs文件支持 URL 路徑。

import './foo?query=1'; // 加載 ./foo 傳入參數 ?query=1

上面代碼中,腳本路徑帶有參數?query=1,Node 會按 URL 規則解讀。同一個腳本只要參數不一樣,就會被加載屢次,而且保存成不一樣的緩存。因爲這個緣由,只要文件名中含有:%#?等特殊字符,最好對這些字符進行轉義。

目前,Node 的import命令只支持加載本地模塊(file:協議),不支持加載遠程模塊。

若是模塊名不含路徑,那麼import命令會去node_modules目錄尋找這個模塊。

import 'baz';
import 'abc/123';

若是模塊名包含路徑,那麼import命令會按照路徑去尋找這個名字的腳本文件。

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.jsonmain字段指定的腳本。若是./foo/package.json不存在或者沒有main字段,那麼就會依次加載./foo/index.mjs./foo/index.js./foo/index.json./foo/index.node。若是以上四個文件仍是都不存在,就會拋出錯誤。

最後,Node 的import命令是異步加載,這一點與瀏覽器的處理方法相同。

內部變量

ES6 模塊應該是通用的,同一個模塊不用修改,就能夠用在瀏覽器環境和服務器環境。爲了達到這個目標,Node 規定 ES6 模塊之中不能使用 CommonJS 模塊的特有的一些內部變量。

首先,就是this關鍵字。ES6 模塊之中,頂層的this指向undefined;CommonJS 模塊的頂層this指向當前模塊,這是二者的一個重大差別。

其次,如下這些頂層變量在 ES6 模塊之中都是不存在的。

  • arguments
  • require
  • module
  • exports
  • __filename
  • __dirname

若是你必定要使用這些變量,有一個變通方法,就是寫一個 CommonJS 模塊輸出這些變量,而後再用 ES6 模塊加載這個 CommonJS 模塊。可是這樣一來,該 ES6 模塊就不能直接用於瀏覽器環境了,因此不推薦這樣作。

// expose.js
module.exports = {__dirname};

// use.mjs
import expose from './expose.js';
const {__dirname} = expose;

上面代碼中,expose.js是一個 CommonJS 模塊,輸出變量__dirname,該變量在 ES6 模塊之中不存在。ES6 模塊加載expose.js,就能夠獲得__dirname

ES6 模塊加載 CommonJS 模塊

CommonJS 模塊的輸出都定義在module.exports這個屬性上面。Node 的import命令加載 CommonJS 模塊,Node 會自動將module.exports屬性,看成模塊的默認輸出,即等同於export default xxx

下面是一個 CommonJS 模塊。

// a.js
module.exports = {
  foo: 'hello',
  bar: 'world'
};

// 等同於
export default {
  foo: 'hello',
  bar: 'world'
};

import命令加載上面的模塊,module.exports會被視爲默認輸出,即import命令實際上輸入的是這樣一個對象{ default: module.exports }

因此,一共有三種寫法,能夠拿到 CommonJS 模塊的module.exports

// 寫法一
import baz from './a';
// baz = {foo: 'hello', bar: 'world'};

// 寫法二
import {default as baz} from './a';
// baz = {foo: 'hello', bar: 'world'};

// 寫法三
import * as baz from './a';
// baz = {
// get default() {return module.exports;},
// get foo() {return this.default.foo}.bind(baz),
// get bar() {return this.default.bar}.bind(baz)
// }

上面代碼的第三種寫法,能夠經過baz.default拿到module.exportsfoo屬性和bar屬性就是能夠經過這種方法拿到了module.exports

下面是一些例子。

// b.js
module.exports = null;

// es.js
import foo from './b';
// foo = null;

import * as bar from './b';
// bar = { default:null };

上面代碼中,es.js採用第二種寫法時,要經過bar.default這樣的寫法,才能拿到module.exports

// c.js
module.exports = function two() {
  return 2;
};

// es.js
import foo from './c';
foo(); // 2

import * as bar from './c';
bar.default(); // 2
bar(); // throws, bar is not a function

上面代碼中,bar自己是一個對象,不能看成函數調用,只能經過bar.default調用。

CommonJS 模塊的輸出緩存機制,在 ES6 加載方式下依然有效。

// foo.js
module.exports = 123;
setTimeout(_ => module.exports = null);

上面代碼中,對於加載foo.js的腳本,module.exports將一直是123,而不會變成null

因爲 ES6 模塊是編譯時肯定輸出接口,CommonJS 模塊是運行時肯定輸出接口,因此採用import命令加載 CommonJS 模塊時,不容許採用下面的寫法。

// 不正確
import { readfile } from 'fs';

上面的寫法不正確,由於fs是 CommonJS 格式,只有在運行時才能肯定readfile接口,而import命令要求編譯時就肯定這個接口。解決方法就是改成總體輸入。

// 正確的寫法一
import * as express from 'express';
const app = express.default();

// 正確的寫法二
import express from 'express';
const app = express();

CommonJS 模塊加載 ES6 模塊

CommonJS 模塊加載 ES6 模塊,不能使用require命令,而要使用import()函數。ES6 模塊的全部輸出接口,會成爲輸入對象的屬性。

// es.mjs
let foo = { bar: 'my-default' };
export default foo;
foo = null;

// cjs.js
const es_namespace = await import('./es');
// es_namespace = {
// get default() {
// ...
// }
// }
console.log(es_namespace.default);
// { bar:'my-default' }

上面代碼中,default接口變成了es_namespace.default屬性。另外,因爲存在緩存機制,es.jsfoo的從新賦值沒有在模塊外部反映出來。

下面是另外一個例子。

// es.js
export let foo = { bar:'my-default' };
export { foo as bar };
export function f() {};
export class c {};

// cjs.js
const es_namespace = await import('./es');
// es_namespace = {
// get foo() {return foo;}
// get bar() {return foo;}
// get f() {return f;}
// get c() {return c;}
// }

循環加載

「循環加載」(circular dependency)指的是,a腳本的執行依賴b腳本,而b腳本的執行又依賴a腳本。

// a.js
var b = require('b');

// b.js
var a = require('a');

一般,「循環加載」表示存在強耦合,若是處理很差,還可能致使遞歸加載,使得程序沒法執行,所以應該避免出現。

可是實際上,這是很難避免的,尤爲是依賴關係複雜的大項目,很容易出現a依賴bb依賴cc又依賴a這樣的狀況。這意味着,模塊加載機制必須考慮「循環加載」的狀況。

對於 JavaScript 語言來講,目前最多見的兩種模塊格式 CommonJS 和 ES6,處理「循環加載」的方法是不同的,返回的結果也不同。

CommonJS 模塊的加載原理

介紹 ES6 如何處理「循環加載」以前,先介紹目前最流行的 CommonJS 模塊格式的加載原理。

CommonJS 的一個模塊,就是一個腳本文件。require命令第一次加載該腳本,就會執行整個腳本,而後在內存生成一個對象。

{
  id: '...',
  exports: { ... },
  loaded: true,
  ...
}

上面代碼就是 Node 內部加載模塊後生成的一個對象。該對象的id屬性是模塊名,exports屬性是模塊輸出的各個接口,loaded屬性是一個布爾值,表示該模塊的腳本是否執行完畢。其餘還有不少屬性,這裏都省略了。

之後須要用到這個模塊的時候,就會到exports屬性上面取值。即便再次執行require命令,也不會再次執行該模塊,而是到緩存之中取值。也就是說,CommonJS 模塊不管加載多少次,都只會在第一次加載時運行一次,之後再加載,就返回第一次運行的結果,除非手動清除系統緩存。

CommonJS 模塊的循環加載

CommonJS 模塊的重要特性是加載時執行,即腳本代碼在require的時候,就會所有執行。一旦出現某個模塊被"循環加載",就只輸出已經執行的部分,還未執行的部分不會輸出。

讓咱們來看,Node 官方文檔裏面的例子。腳本文件a.js代碼以下。

exports.done = false;
var b = require('./b.js');
console.log('在 a.js 之中,b.done = %j', b.done);
exports.done = true;
console.log('a.js 執行完畢');

上面代碼之中,a.js腳本先輸出一個done變量,而後加載另外一個腳本文件b.js。注意,此時a.js代碼就停在這裏,等待b.js執行完畢,再往下執行。

再看b.js的代碼。

exports.done = false;
var a = require('./a.js');
console.log('在 b.js 之中,a.done = %j', a.done);
exports.done = true;
console.log('b.js 執行完畢');

上面代碼之中,b.js執行到第二行,就會去加載a.js,這時,就發生了「循環加載」。系統會去a.js模塊對應對象的exports屬性取值,但是由於a.js尚未執行完,從exports屬性只能取回已經執行的部分,而不是最後的值。

a.js已經執行的部分,只有一行。

exports.done = false;

所以,對於b.js來講,它從a.js只輸入一個變量done,值爲false

而後,b.js接着往下執行,等到所有執行完畢,再把執行權交還給a.js。因而,a.js接着往下執行,直到執行完畢。咱們寫一個腳本main.js,驗證這個過程。

var a = require('./a.js');
var b = require('./b.js');
console.log('在 main.js 之中, a.done=%j, b.done=%j', a.done, b.done);

執行main.js,運行結果以下。

$ node main.js

在 b.js 之中,a.done = false
b.js 執行完畢
在 a.js 之中,b.done = true
a.js 執行完畢
在 main.js 之中, a.done=true, b.done=true

上面的代碼證實了兩件事。一是,在b.js之中,a.js沒有執行完畢,只執行了第一行。二是,main.js執行到第二行時,不會再次執行b.js,而是輸出緩存的b.js的執行結果,即它的第四行。

exports.done = true;

總之,CommonJS 輸入的是被輸出值的拷貝,不是引用。

另外,因爲 CommonJS 模塊遇到循環加載時,返回的是當前已經執行的部分的值,而不是代碼所有執行後的值,二者可能會有差別。因此,輸入變量的時候,必須很是當心。

var a = require('a'); // 安全的寫法
var foo = require('a').foo; // 危險的寫法

exports.good = function (arg) {
  return a.foo('good', arg); // 使用的是 a.foo 的最新值
};

exports.bad = function (arg) {
  return foo('bad', arg); // 使用的是一個部分加載時的值
};

上面代碼中,若是發生循環加載,require('a').foo的值極可能後面會被改寫,改用require('a')會更保險一點。

ES6 模塊的循環加載

ES6 處理「循環加載」與 CommonJS 有本質的不一樣。ES6 模塊是動態引用,若是使用import從一個模塊加載變量(即import foo from 'foo'),那些變量不會被緩存,而是成爲一個指向被加載模塊的引用,須要開發者本身保證,真正取值的時候可以取到值。

請看下面這個例子。

// a.mjs
import {bar} from './b';
console.log('a.mjs');
console.log(bar);
export let foo = 'foo';

// b.mjs
import {foo} from './a';
console.log('b.mjs');
console.log(foo);
export let bar = 'bar';

上面代碼中,a.mjs加載b.mjsb.mjs又加載a.mjs,構成循環加載。執行a.mjs,結果以下。

$ node --experimental-modules a.mjs
b.mjs
ReferenceError: foo is not defined

上面代碼中,執行a.mjs之後會報錯,foo變量未定義,這是爲何?

讓咱們一行行來看,ES6 循環加載是怎麼處理的。首先,執行a.mjs之後,引擎發現它加載了b.mjs,所以會優先執行b.mjs,而後再執行a.js。接着,執行b.mjs的時候,已知它從a.mjs輸入了foo接口,這時不會去執行a.mjs,而是認爲這個接口已經存在了,繼續往下執行。執行到第三行console.log(foo)的時候,才發現這個接口根本沒定義,所以報錯。

解決這個問題的方法,就是讓b.mjs運行的時候,foo已經有定義了。這能夠經過將foo寫成函數來解決。

// a.mjs
import {bar} from './b';
console.log('a.mjs');
console.log(bar());
function foo() { return 'foo' }
export {foo};

// b.mjs
import {foo} from './a';
console.log('b.mjs');
console.log(foo());
function bar() { return 'bar' }
export {bar};

這時再執行a.mjs就能夠獲得預期結果。

$ node --experimental-modules a.mjs
b.mjs
foo
a.mjs
bar

這是由於函數具備提高做用,在執行import {bar} from './b'時,函數foo就已經有定義了,因此b.mjs加載的時候不會報錯。這也意味着,若是把函數foo改寫成函數表達式,也會報錯。

// a.mjs
import {bar} from './b';
console.log('a.mjs');
console.log(bar());
const foo = () => 'foo';
export {foo};

上面代碼的第四行,改爲了函數表達式,就不具備提高做用,執行就會報錯。

咱們再來看 ES6 模塊加載器SystemJS給出的一個例子。

// even.js
import { odd } from './odd'
export var counter = 0;
export function even(n) {
  counter++;
  return n === 0 || odd(n - 1);
}

// odd.js
import { even } from './even';
export function odd(n) {
  return n !== 0 && even(n - 1);
}

上面代碼中,even.js裏面的函數even有一個參數n,只要不等於 0,就會減去 1,傳入加載的odd()odd.js也會作相似操做。

運行上面這段代碼,結果以下。

$ babel-node
> import * as m from './even.js';
> m.even(10);
true
> m.counter
6
> m.even(20)
true
> m.counter
17

上面代碼中,參數n從 10 變爲 0 的過程當中,even()一共會執行 6 次,因此變量counter等於 6。第二次調用even()時,參數n從 20 變爲 0,even()一共會執行 11 次,加上前面的 6 次,因此變量counter等於 17。

這個例子要是改寫成 CommonJS,就根本沒法執行,會報錯。

// even.js
var odd = require('./odd');
var counter = 0;
exports.counter = counter;
exports.even = function (n) {
  counter++;
  return n == 0 || odd(n - 1);
}

// odd.js
var even = require('./even').even;
module.exports = function (n) {
  return n != 0 && even(n - 1);
}

上面代碼中,even.js加載odd.js,而odd.js又去加載even.js,造成「循環加載」。這時,執行引擎就會輸出even.js已經執行的部分(不存在任何結果),因此在odd.js之中,變量even等於null,等到後面調用even(n-1)就會報錯。

$ node
> var m = require('./even');
> m.even(10)
TypeError: even is not a function

ES6 模塊的轉碼

瀏覽器目前還不支持 ES6 模塊,爲了如今就能使用,能夠將轉爲 ES5 的寫法。除了 Babel 能夠用來轉碼以外,還有如下兩個方法,也能夠用來轉碼。

ES6 module transpiler

ES6 module transpiler是 square 公司開源的一個轉碼器,能夠將 ES6 模塊轉爲 CommonJS 模塊或 AMD 模塊的寫法,從而在瀏覽器中使用。

首先,安裝這個轉碼器。

$ npm install -g es6-module-transpiler

而後,使用compile-modules convert命令,將 ES6 模塊文件轉碼。

$ compile-modules convert file1.js file2.js

-o參數能夠指定轉碼後的文件名。

$ compile-modules convert -o out.js file1.js

SystemJS

另外一種解決方法是使用 SystemJS。它是一個墊片庫(polyfill),能夠在瀏覽器內加載 ES6 模塊、AMD 模塊和 CommonJS 模塊,將其轉爲 ES5 格式。它在後臺調用的是 Google 的 Traceur 轉碼器。

使用時,先在網頁內載入system.js文件。

<script src="system.js"></script>

而後,使用System.import方法加載模塊文件。

<script> System.import('./app.js'); </script>

上面代碼中的./app,指的是當前目錄下的 app.js 文件。它能夠是 ES6 模塊文件,System.import會自動將其轉碼。

須要注意的是,System.import使用異步加載,返回一個 Promise 對象,能夠針對這個對象編程。下面是一個模塊文件。

// app/es6-file.js:

export class q {
  constructor() {
    this.es6 = 'hello';
  }
}

而後,在網頁內加載這個模塊文件。

<script> System.import('app/es6-file').then(function(m) { console.log(new m.q().es6); // hello }); </script>

上面代碼中,System.import方法返回的是一個 Promise 對象,因此能夠用then方法指定回調函數。