零.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.jsとapp.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 …;
export と export 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 を参照
二.ロードメカニズム
つまり:
-
type="module"のリソースはdefer効果を自带(HTML 文書解析完了後に実行) -
asyncは依然有効(リソースロード完了後直ちに実行。実行完了後 HTML 文書解析を継続) -
importリソースロードは並行
自带 defer 効果。裸 script のデフォルト動作(リソースロード直ちに実行。かつ HTML 文書解析をブロック)と異なる。另外、import が同級リソースをロードするのは並行だが、下一级依存を探すプロセスは不可避免地順序串行。这部分パフォーマンスは無視できない。ブラウザが原生で ES Module をサポートしても、勝手に import できない
CSS 中の @import ルールに類似。最佳実践が発展する可能性。モジュール化とロードパフォーマンスの間でバランスを追求
三.特徴
1.静的メカニズム
if、try-catch 文、関数または eval などの場所で import を使用できない。モジュール最外層にのみ出現可能
并且 import には提升(Hosting)特性がある。変数宣言が現在のスコープ顶部に提升されるのと同じように、モジュールに宣言された import はモジュール顶部に提升される
P.S. 静的モジュールメカニズムは解析/実行最適化に有利
2.新しい script タイプ
新しい script タイプ属性 type="module" が必要。解析器は内容が ES Module かどうかを推測できないため(例えば import, export キーワードがない。厳格モードにも従う。ではモジュールとみなすか?)
另外、内容で推測するのは多次元解析のパフォーマンス損失が存在
3.モジュールスコープ
各モジュールは自分のスコープを持つ。モジュール下の変数宣言はグローバルに暴露しない
4.デフォルトで厳格モードを开启
this は global を指さず、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 を参照
コメントはまだありません