メインコンテンツへ移動

ES Module

無料2017-09-03#JS#es6 module#es6 import#es module and cjs#es module current state#js模块

2 年待った Demo がようやく実行できるようになりました

零.7 種類のモジュール化方式

1.セクション注釈

<!--html-->
<script>
    // module1 code
    // module2 code
</script>

手動で注釈を追加してモジュール範囲を示す。CSS 内のセクション注釈に類似:

/* -----------------
 * TOOLTIPS
 * ----------------- */

唯一の作用はコード閲覧を容易にすること。指定モジュールを素早く見つける。根本原因は単一ファイルの内容が長すぎて、すでにメンテナンスの麻烦に遭遇しているため、手動でいくつかのアンカーを挿入して快速ジャンプ用

非常に原始的なモジュール化方案。実質的な利点はない(モジュールスコープ、依存処理、モジュール間エラー隔離など)

2.複数 script タグ

<!--html-->
<script type="application/javascript" src="PATH/polyfill-vendor.js" ></script>
<script type="application/javascript" src="PATH/module1.js" ></script>
<script type="application/javascript" src="PATH/module2.js" ></script>
<script type="application/javascript" src="PATH/app.js" ></script>

各モジュールを独立ファイルに拆分。3 つの利点がある:

  • リソースロード順序を制御してモジュール依存を処理

  • モジュール間エラー隔離がある(module1.js 初期化実行異常は module2.jsapp.js の実行を阻断しない)

  • 各モジュールは別ファイルに位置。実際にメンテナンス体験を向上

しかし 2 つの問題がまだ存在:

  • モジュールスコープがない

  • リソースリクエスト数量はモジュール化粒度と関連。パフォーマンスとモジュール化収益のバランスを探す必要

3.IIFE

const myModule = (function (...deps){
   // JavaScript chunk
   return {hello : () => console.log('hello from myModule')};
})(dependencies);

パッチとして使用可能。他の方式と配合して使用。モジュールスコープを提供

4.Asynchronous module definition (AMD)

RequireJS 例:

// polyfill-vendor.js
define(function () {
    // polyfills-vendor code
});

// module1.js
define(function () {
    //...
    return module1;
});
// module2.js
define(function () {
    //...
    return module2;
});

// app.js
define(['PATH/polyfill-vendor'] , function () {
    define(['PATH/module1', 'PATH/module2'] , function (module1, module2) {
        var APP = {};

        if (isModule1Needed) {
            APP.module1 = module1({param: 1});
        }
        APP.module2 = new module2({a: 42});
    });
});

比較的完善なモジュール定義方案。モジュール依存問題を解決。モジュールスコープ、エラー隔離/捕獲などの方案を提供。しかし少し冗長に見える

P.S. 他に SeaJS もある(公式サイトはもうない。紹介しない)。コミュニティ実装のモジュール化パッチはすべて過渡産物。現在看来、JS はついにモジュール化特性を迎えるようだ

5.CommonJS

NodeJS 例:

// polyfill-vendor.js
    // polyfills-vendor code

// module1.js
    // module1 code
    module.exports= module1;
// module2.js
module.exports= module2;

// app.js
require('PATH/polyfill-vendor');

const module1 = require('PATH/module1');
const module2 = require('PATH/module2');

const APP = {};
if(isModule1Needed){
    APP.module1 = module1({param:1});
}
APP.module2 = new module2({a: 42});

NodeJS は CommonJS 規範に従う。ファイル即モジュール。同様に比較的完善な方案。しかしブラウザ環境には適用しない

6.UMD (Universal Module Dependency)

UMD 例:

(function (global, factory) {
    typeof exports === 'object' && typeof module !== 'undefined' ? factory() :
    typeof define === 'function' && define.amd ? define(factory) :
(factory());
}(this, function () {
    // JavaScript chunk
    return {
       hello : () => console.log('hello from myModule')
    }
});

同様にパッチ。AMD と CommonJS モジュール定義を互換。モジュール跨環境通用を実現。UMD が出現した根本原因はコミュニティモジュール定義方式が多すぎて、オープンソースモジュールメンテナンスが非常に麻烦(各種 MD issue が出現。仕方なく UMD に交換)。そのため標準化が非常に迫切。ES6 はこの使命を担う

P.S. もちろん、オープンソースモジュールのメンテナンス問題はまだ存在(ES Module に迎合するため、また専用の ES6 構築バージョンを追加)。しかし加剧しない。畢竟すでに標準化の路上にある

7.ES6 Module

基本用法例:

// myModule.js
export {fn1, fn2};

function fn1() {
    console.log('fn1');
}
function fn2() {
    console.log('fn2');
}

// app.js
import {fn1, fn2} from './myModule.js';
fn1();
fn2();

// index.html
<script type="module" src="app.js"></script>

注意

  • script タグは必ず type="module" を宣言して ES Module 方式で内容を解析することを示す。否则実行されない

  • import モジュールファイル正確なパス./)、ファイル拡張子.js)及び対応のMIME タイプが必ず必要。否则導入失敗

現在各大主流ブラウザはすべて ES Module 実験性功能を提供:

  • Safari 10.1.

  • Chrome Canary 60 – behind the Experimental Web Platform flag in chrome:flags.

  • Firefox 54 – behind the dom.moduleScripts.enabled setting in about:config.

  • Edge 15 – behind the Experimental JavaScript Features setting in about:flags.

2 年待った Demo がようやく実行できるようになりました:http://ayqy.net/temp/module/index.html

P.S. 一般に ES Module と呼ぶ。Module 特性は複数のバージョンが存在しないため。ES Module は ES6 が導入した Module 特性を指す

一.構文

export

// 基本構文
export { name1, name2, …, nameN };
export { variable1 as name1, variable2 as name2, …, nameN };
export let name1, name2, …, nameN; // also var, function
export let name1 = …, name2 = …, …, nameN; // also var, const

// デフォルト輸出
export default expression;
export default function (…) { … } // also class, function*
export default function name1(…) { … } // also class, function*
export { name1 as default, … };

// 聚合輸出
export * from …;
export { name1, name2, …, nameN } from …;
export { import1 as name1, import2 as name2, …, nameN } from …;

exportexport default の違いに注意:

  • 各モジュール(/ファイル)は 1 つの export default のみ。複数の export が可能

  • export default 後は任意の式を接続可能。export 構文は 3 種類のみ

例えば:

// 不合法。構文エラー
export {
    a: 1
};
// 而应该用 export { name1, name2, …, nameN };
let a = 1;
export {
    a
};
// または export let name1 = …, name2 = …, …, nameN; // also var, const
export let a = 1;

デフォルト輸出

デフォルト輸出は一種の特殊な輸出形式。例えば:

// module.js
export {fn1, fn2};
function fn1() {
    console.log('fn1');
}
function fn2() {
    console.log('fn2');
}
export default {
    a: 1
};
let b = 2;
export {
    b
};
export let c = 3;

// app.js
import * as m from './module.js';
console.log(m);
// 出力結果
Module {
    b: 2,
    c: 3,
    default: {
        a: 1
    },
    fn1: ?n1,
    fn2: ?n2
}

デフォルト輸出は Module オブジェクトの default 属性に隔離される。他の export と待遇が異なる

聚合輸出

import + export に相当。しかし現在のモジュールスコープに各 API 変数を導入しない(導入後直接輸出。引用不可)。API 聚合の中転作用のみ。例えば:

// lib.js
let util = {name: 'util'};
let dialog = {name: 'core'};
let modal = {name: 'modal'};

export {
    util,
    dialog,
    modal
}

// module.js
console.log(`before export from lib: ${typeof dialog}`);
export * from './lib.js';
console.log(`after export from lib: ${typeof dialog}`);

前後とも undefined。畢竟中転のみ。現在のモジュールスコープに導入しない。而 import + export は先に導入。現在のモジュールで使用可能

import

// default export 内容を導入
import defaultMember from "module-name";
// すべての export 内容を導入。default を含み、名为 name のオブジェクトにパック
import * as name from "module-name";
// 名前で指定 export 内容を導入
import { member } from "module-name";
import { member as alias } from "module-name";
import { member1, member2 } from "module-name";
import { member1, member2 as alias2 , [...] } from "module-name";
// default export 内容を導入。同時に名前で指定 export 内容を導入
import defaultMember, { member [ , [...] ] } from "module-name";
import defaultMember, * as name from "module-name";
// モジュールに暴露するものを導入しない。該モジュールコードのみ実行
import "module-name";

最後一种是面白い。Import a module for its side effects only と呼ばれる。モジュールコードのみ実行。何も新しいものを導入しない(外部状態に影響する部分のみ生效。即副作用)

P.S. ES Module 構文に関する詳細情報は、[module_ES6 筆記 13](/articles/module-es6 筆記 13/)、または参考資料部分の ES Module Spec を参照

P.S. NodeJS も ES Module サポートを検討。しかし CommonJS モジュールと ES Module をどう区別するかという問題に遭遇。まだ討論中。詳細情報は ES Module Detection in Node を参照

二.ロードメカニズム

ES6 モジュールロードメカニズム示意图

つまり:

  • type="module" のリソースは defer 効果を自带(HTML 文書解析完了後に実行)

  • async は依然有効(リソースロード完了後直ちに実行。実行完了後 HTML 文書解析を継続)

  • import リソースロードは並行

自带 defer 効果。裸 script のデフォルト動作(リソースロード直ちに実行。かつ HTML 文書解析をブロック)と異なる。另外、import が同級リソースをロードするのは並行だが、下一级依存を探すプロセスは不可避免地順序串行。这部分パフォーマンスは無視できない。ブラウザが原生で ES Module をサポートしても、勝手に import できない

CSS 中の @import ルールに類似。最佳実践が発展する可能性。モジュール化とロードパフォーマンスの間でバランスを追求

三.特徴

1.静的メカニズム

iftry-catch 文、関数または eval などの場所で import を使用できない。モジュール最外層にのみ出現可能

并且 import には提升(Hosting)特性がある。変数宣言が現在のスコープ顶部に提升されるのと同じように、モジュールに宣言された import はモジュール顶部に提升される

P.S. 静的モジュールメカニズムは解析/実行最適化に有利

2.新しい script タイプ

新しい script タイプ属性 type="module" が必要。解析器は内容が ES Module かどうかを推測できないため(例えば import, export キーワードがない。厳格モードにも従う。ではモジュールとみなすか?)

另外、内容で推測するのは多次元解析のパフォーマンス損失が存在

3.モジュールスコープ

各モジュールは自分のスコープを持つ。モジュール下の変数宣言はグローバルに暴露しない

4.デフォルトで厳格モードを开启

thisglobal を指さず、undefined である

5.Data URI と Blob URI をサポート

import grape from 'data:text/javascript,export default "grape"';

// 空の ES モジュールを作成
const scriptAsBlob = new Blob([''], {
    type: 'application/javascript'
});
const srcObjectURL = URL.createObjectURL(scriptAsBlob);
 // ES モジュールを挿入し、イベントをリスン
const script = document.createElement('script');
script.type = 'module';
document.head.appendChild(script);
// スクリプトのロードを開始
script.src = srcObjectURL;

6.CORS 制限を受ける

跨域のモジュールリソースは import 導入できない。script タグでモジュール方式で��ードすることもできない

7.HTTPS リソースは HTTP リソースを import できない

HTTPS ページが HTTP リソースをロードするのと類似。block される

8.モジュールは単例

普通の script と異なる。導入されるモジュールは単例(1 回のみ実行)。import でも type="module"script タグで導入しても

9.モジュールリソースをリクエストする際に身元証憑(credentials)を帯びない

Fetch API と脾气が同じ。デフォルトで身元証憑を帯びない。script タグに crossorigin 属性を追加する必要

四.問題

1.import エラー

正確なモジュールファイルパスを给出する必要がある。否则モジュール内容を実行しない。并且 Chrome 60 はエラー報告もない

P.S. import エラーは現在各ブラウザにまだ差異が存在

2.モジュール間エラー隔離は依然として問題

リソースロードエラー:動的に script を挿入してモジュールをロード。onerror でロード異常をリスン

モジュール初期化エラー:window.onerror グローバル捕獲。エラー情報を通じてモジュール名を見つけようとする。モジュール初期化失敗を記録

3.リクエスト数量爆発

例えば lodash demo。600 以上のファイルをロードする必要がある

HTTP2 を上げれば碎ファイルの問題を緩和できる。しかし根源から見ると、生産環境に適用する最佳実践の一套が必要。モジュール化の粒度を規範

4.動的 import

現在まだ実現していない。import() API が专门にこの問題を解決。規範はまだ草案第 3 段階。詳細情報は Native ECMAScript modules: dynamic import() を参照

5.モジュール環境検出

現在の執行環境がモジュールかどうかをチェック:

const inModule = this === undefined;

あまり信頼できないように見える。しかしどうやらこれしかできない。document.currentScript は ES Module では null であるため。type チェックができない

五.ダウングレード方案

1.特性検出

特性検出を一遍通す。環境検出 util からモジュールを導入。非常に手間がかかりパフォーマンスを損なう。例えば malyw/es-modules-utils

typeof は通じない。import, export はキーワードであるため。type="module"script タグを挿入可能。空モジュールをロード(Blob URI または Data URI を使用可能)。onload をトリガーすればサポートを示す

另外一種の取巧な方法がある:

<script type="module">
    window.__browserHasModules = true;
</script>

このようなモジュールを導入して特性検出を行う。しかし ES Module は自带 defer 効果のため。執行順序を保証するために、後続のすべての JS リソースは defer 属性を持つ必要がある(ダウングレード用の正常バージョンを含む)

2.nomodule

nomodule 属性。作用は noscript タグに類似。<script nomodule>console.log('ES Module をサポートしない環境でのみ実行')</script>

しかしブラウザサポートに依存。該属性をサポートしないが ES Module をサポートする環境では問題がある(両方とも実行)。すでに HTML 規範に追加された。しかし現在互換性はまだ非常に悪い

  • Firefox 最新版はサポート

  • Edge はサポートしない

  • Safari 10.1 はサポートしない。しかし 方法 で解決可能

  • Chrome 60 はサポート

ダウングレード方案に関する詳細情報は、Native ECMAScript modules: nomodule attribute for the migration を参照

参考資料

コメント

コメントはまだありません

コメントを書く