ES6のモジュールシステム
この記事は翻訳です。原文のリンクはこちら:https://hacks.mozilla.org/2015/08/es6-in-depth-modules/
ES6はECMAScript第6版の略称で、これは新世代のJavaScriptの標準です。ES6 in DepthはES6の一連の新機能の紹介です。
2007年、筆者がMozillaのJavaScriptチームで働き始めた頃、典型的なJavaScriptプログラムは1行のコードだけでした。
2年後、Google Mapが公開されました。しかしそれ以前は、JavaScriptの主な用途はフォームの検証であり、もちろんあなたの<input onchange=>ハンドラーは平均して1行のコードでした。
時が経ち、JavaScriptプロジェクトは非常に大規模になり、コミュニティは拡張可能なプログラムを開発するためのツールをいくつか開発しました。まず必要なのはモジュールシステムです。モジュールシステムは、作業を異なるファイルやディレクトリに分散させ、相互にアクセスできるようにし、それらを非常に効率的にロードできるようにします。自然に、JavaScriptはモジュールシステムを発展させ、実際には複数のモジュールシステム(AMD、CommonJS、CMD、訳者注)を持っています。さらに、コミュニティはパッケージ管理ツール(NPM、訳者注)を提供し、他のモジュールに高度に依存するソフトウェアをインストールおよびコピーできるようにしました。モジュール機能を持つES6は、少し遅れてきたと感じるかもしれません。
モジュールの基本
ES6のモジュールは、JSコードを含むファイルです。ES6にはmoduleというキーワードはありません。モジュールは通常のスクリプトファイルと見た目は同じですが、以下の2つの違いがあります:
- ES6のモジュールは自動的に厳格モードが有効になります。たとえ
'use strict'を書かなくても。 - モジュール内で
importとexportを使用できます。
まずexportを見てみましょう。モジュール内で宣言されたものはすべてデフォルトでプライベートです。他のモジュールに公開したい場合は、その部分のコードをexportする必要があります。いくつかの実装方法がありますが、最も簡単な方法はexportキーワードを追加することです。
// kittydar.js - 画像内のすべての猫の位置を見つける。
// (Heather Arthurがこのライブラリを実際に書きました)
// (しかし彼女はモジュールを使用しませんでした、なぜならそれは2013年だったから)
export function detectCats(canvas, options) {
var kittydar = new Kittydar(options);
return kittydar.detectCats(canvas);
}
export class Kittydar {
... いくつかの画像処理メソッド ...
}
// このヘルパー関数はエクスポートされません。
function resizeCanvas() {
...
}
...
function、class、var、let、またはconstの前にexportを追加できます。
モジュールを書く場合、これだけで十分です!もうIIFEやコールバック関数にコードを入れる必要はありません。あなたのコードはモジュールであり、スクリプトファイルではないので、宣言したすべてのものはモジュールのスコープにカプセル化され、モジュール間やファイル間のグローバル変数は存在しなくなります。エクスポートされた宣言部分は、このモジュールのPublic APIになります。
さらに、モジュール内のコードは通常のコードと大きな違いはありません。基本的なグローバル変数(例えばObjectやArray)にアクセスできます。モジュールがブラウザで実行される場合、documentやXMLHttpRequestにアクセスできます。
別のファイルで、このモジュールをインポートし、detectCats()関数を使用できます:
// demo.js - Kittydarデモプログラム
import { detectCats } from 'kittydar.js'
function go() {
var canvas = document.getElementById('catpix')
var cats = detectCats(canvas)
drawRectangles(canvas, cats)
}
複数のモジュールからインターフェースをインポートする場合、次のように書けます:
import { detectCats, Kittydar } from 'kittydar.js'
import宣言を含むモジュールを実行すると、インポートされたモジュールは最初にインポートされ、ロードされ、その後依存関係に基づいて、各モジュールの内容が深さ優先の原則に従って探索されます。すでに実行されたモジュールはスキップされ、依存関係の循環を回避します。
これがモジュールの基本部分です。非常にシンプルです。
エクスポートリスト
エクスポートする部分の前にすべてexportを書くのが面倒だと思った場合、エクスポートしたい変数のリストを1行だけ書き、波括弧で囲むことができます。
export {detectCats, Kittydar};
// ここでは`export`キーワードは必要ありません
function detectCats(canvas, options) { ... }
class Kittydar { ... }
エクスポートリストはファイルの最初の行に出現する必要はなく、モジュールのトップレベルスコープの任意の行に出現できます。複数のエクスポートリストを書くこともでき、リストに他のexport宣言を追加することもできますが、変数名が重複してエクスポートされない限りです。
名前の衝突するエクスポートとインポート
インポートする変数名がモジュール内の変数名と衝突する場合、ES6はインポートしたものに名前を付け直すことを許可します:
// suburbia.js
// これらのモジュールはどちらも`flip`という名前のものをエクスポートします。
// 両方をインポートするには、少なくとも1つの名前を変更する必要があります。
import {flip as flipOmelet} from "eggs.js";
import {flip as flipHouse} from "real-estate.js";
...
同様に、エクスポートする変数の名前も変更できます。この機能は、同じ変数名を2回エクスポートしたい場合に非常に便利です。例えば:
// unlicensed_nuclear_accelerator.js - drmなしのメディアストリーミング
// (実際のライブラリではありませんが、そうあるべきかもしれません)
function v1() { ... }
function v2() { ... }
export {
v1 as streamV1,
v2 as streamV2,
v2 as streamLatestVersion
};
デフォルトエクスポート
新世代の標準の設計理念の1つは、既存のCommonJSおよびAMDモジュールとの互換性です。したがって、Nodeプロジェクトがあり、npm install lodashを実行したばかりの場合、ES6コードはLodashの関数を独立してインポートできます:
import { each, map } from 'lodash'
each([3, 2, 1], (x) => console.log(x))
しかし、もしあなたが_.eachに慣れているか、_が見えないと気持ち悪い場合、もちろんこのようにLodashを使用するのも良い方法です。
この場合、インポートの書き方を少し変更し、波括弧を使わないことができます:
import _ from 'lodash'
この省略形はimport {default as _} from "lodash";と同等です。すべてのCommonJSおよびAMDモジュールは、ES6コードで使用されるときにデフォルトエクスポートを持っており、このエクスポートはCommonJSでrequire()したものと同じで、つまりexportsオブジェクトです。
ES6のモジュールシステムは、複数の変数を一度にインポートできるように設計されています。しかし、既存のCommonJSモジュールに対しては、デフォルトエクスポートしか得られません。例えば、この記事を書いている時点で、著名なcolorsモジュールはES6を特にサポートしていないことが知られています。これは、npm上のパッケージのように、複数のCommonJSモジュールで構成されています。しかし、それでもES6コードに直接インポートすることができます。
// ES6の`var colors = require("colors/safe");`に相当します
import colors from 'colors/safe'
独自のデフォルトエクスポートを書くのも非常に簡単です。特に高科技はなく、通常のエクスポートと何も変わりません。ただし、エクスポート名はdefaultです。以前に紹介した構文を使用できます:
let myObject = {
field1: value1,
field2: value2,
}
export { myObject as default }
これがより良いです:
export default {
field1: value1,
field2: value2,
}
export defaultキーワードの後には、任意の値(関数、オブジェクト、オブジェクトリテラル、あなたが言えるものなら何でも)を続けることができます。
モジュールオブジェクト
申し訳ありませんが、この記事の内容は少し多いですが、JavaScriptは良いものです:いくつかの理由から、すべての言語のモジュールシステムには役に立たない特徴がたくさんあります。幸いなことに、私たちは1つのトピックについて話すだけです。ええと、まあ、2つです。
import * as cows from 'cows'
import *を使用すると、インポートされるのはmodule namespace objectです。その属性はそのモジュールのエクスポートです。したがって、cowsモジュールがmoo()という名前の関数をエクスポートしている場合、cowsをこのようにインポートした後、cows.moo()と書くことができます。
集約モジュール
時には、パッケージのメインモジュールが多くの他のモジュールをインポートし、それらを統一された方法でエクスポートします。このようなコードを簡素化するために、インポートとエクスポートの省略形があります:
// world-foods.js - 世界中の良いもの
// "sri-lanka"をインポートし、そのエクスポートの一部を再エクスポート
export { Tea, Cinnamon } from 'sri-lanka'
// "equatorial-guinea"をインポートし、そのエクスポートの一部を再エクスポート
export { Coffee, Cocoa } from 'equatorial-guinea'
// "singapore"をインポートし、そのすべてのエクスポートをエクスポート
export * from 'singapore'
このexport-fromの表現は、後にexportが続くimport-fromの表現に似ています。しかし、実際のインポートとは異なり、作用域に二次エクスポートの変数バインディングを追加しません。したがって、world-foods.jsでTeaを使用するコードを書く予定がある場合、この省略形を使用しないでください。
もし"singapore"がエクスポートした変数の1つが他のエクスポート変数名と衝突した場合、ここでエラーが発生します。したがって、export *の使用には注意が必要です。
ふぅ!文法の紹介が終わりました。次は面白い部分に入ります。
importは一体何をしているのか
何もしていません、信じるかどうかはあなた次第です。
ああ、あなたはあまり騙されていないようですね。さて、標準がimportが何をすべきかについてほとんど言及していないと信じますか?それは良いことだと思いますか、それとも悪いことだと思いますか?
ES6はモジュールの読み込みの詳細を実装に完全に委ねています、残りの実行部分は非常に詳細に規定されています。
大まかに言えば、JSエンジンがモジュールを実行するとき、その動作は以下の4つのステップに要約できます:
- 解析:エンジンの実装はモジュールのソースコードを読み、構文エラーがないか確認します。
- ロード:エンジンの実装は(再帰的に)すべてのインポートされたモジュールをロードします。この部分はまだ標準化されていません。
- リンク:エンジンの実装は新しくロードされた各モジュールのためにスコープを作成し、モジュール内の宣言をその中にバインドします。他のモジュールからインポートされたものも含まれます。
import {cake} from "paleo"を試みたが、"paleo"モジュールがcakeという名前のものをエクスポートしていない場合、この時点でエラーが発生します。これは悪いことです。成功してcakeを味わうまであと一歩ですから!
- 実行:ついに、JSエンジンは新しくロードされたモジュール内のコードを実行し始めます。この時点で、
importの処理は完了しているため、JSエンジンがimport宣言の行に到達したとき、何も行いません。見ましたか?私は「import “何もしていない”」と言いましたが、あなたを騙してはいませんよね?プログラミング言語に関する真剣な話題について、私は嘘をつきません。
しかし、今からこのシステムの面白い部分を紹介できます。これは非常にクールなトリックです。仕様が読み込みの詳細を指定していないため、また、ソースコード内の import 宣言を一目見るだけでモジュールの依存関係を事前に理解できるため、あるES6の実装では、事前処理によってすべての作業を完了し、モジュールをすべて1つのファイルにパッケージ化し、最後にネットワークを通じて配布することさえ可能です。例えば、webpack のようなツールがこの作業を行っています。
これは非常に素晴らしいことです。なぜなら、ネットワークからリソースを読み込むのは非常に時間がかかるからです。リソースをリクエストし、その中に import 宣言があることがわかると、さらに多くのリソースをリクエストしなければならず、さらに時間がかかります。naiveなローダーの実装では、多くのネットワークリクエストを発生させる可能性があります。しかし、webpackを使用すれば、今日からES6を使用し始めることができ、モジュール化のすべての利点を享受しながら、実行時のパフォーマンスを妥協することはありません。
もともと、私たちは詳細に定義されたES6モジュールの読み込み規範を計画していましたが、それを実現しました。最終的な標準にならなかった理由の1つは、バンドリングという特性と調和できなかったからです。モジュールシステムは標準化される必要があり、バンドリングも放棄されるべきではありません。なぜなら、それは素晴らしいからです。
動的VS静的、または:規則と規則を破る方法
動的プログラミング言語として、JavaScriptは驚くべきことに静的なモジュールシステムを持っています。
importとexportはトップレベルのスコープでのみ記述できます。条件文の中でインポートやエクスポートを使用することはできず、自分が書いた関数のスコープ内でimportを使用することもできません。- すべてのエクスポートは明示的に変数名を指定する必要があり、ループを通じて動的に一連の変数をインポートすることはできません。
- モジュールオブジェクトはカプセル化されており、新しい機能をハックするためにポリフィルを使用することはできません。
- モジュールコードが実行される前に、すべてのモジュールは読み込み、解析、リンクのプロセスを経る必要があります。遅延読み込みや惰性
importの構文はありません。 importエラーについては、実行時にリカバリーすることはできません。アプリケーションには数百のモジュールが含まれている可能性があり、その中のどれかが読み込みに失敗したりリンクに失敗したりすると、そのアプリケーションは実行されません。try/catch文の中でimportすることはできません。(しかし、ES6のモジュールシステムが非常に静的であるため、webpackは事前処理時にこれらのエラーを検出できます)。- モジュールをフックして、それが読み込まれる前に自分のコードを実行することはできません。これは、モジュールが依存関係がどのように読み込まれるかを制御できないことを意味します。
あなたの要求がすべて静的である限り、このモジュールシステムは非常に良いものです。しかし、あなたはハックしたいと思っていますよね?
だからこそ、あなたが使用しているモジュールローディングシステムはAPIを提供するかもしれません。例えば、webpackにはAPIがあり、「コードスプリッティング」を行い、あなたのニーズに応じてモジュールを遅延読み込みすることができます。このAPIは、上記のすべての規則を破るのにも役立ちます。
ES6のモジュールは非常に静的であり、これは素晴らしいことです。多くの強力なコンパイラツールがこれによって恩恵を受けています。そして、静的な構文は動的でプログラム可能なローダーAPIと協調して動作するように設計されています。
いつES6モジュールを使い始められますか?
もし今日から使い始めるのであれば、Traceur や Babel のような事前処理ツールが必要です。このシリーズの以前の記事では、BabelとBroccoliを使用する方法についても紹介しています。その記事の例はGitHubでオープンソース化されています。著者のこの記事も、Babelとwebpackの使用方法について説明しています。
ES6モジュールシステムの主な設計者はDave HermanとSam Tobin-Hochstadtであり、彼らは著者を含む数人の委員の反対にもかかわらず、現在見られるES6モジュールシステムの静的部分を貫き、数年間議論を重ねました。Jon CoppeardはFirefoxブラウザでES6のモジュールを実装しています。その後、JavaScript Loader仕様を含む作業が進行中です。HTML内の <script type=module> のようなものも、皆さんにお目にかかることになるでしょう。
これがES6です。
ES6についての批評を歓迎します。来週のES6 in Depthシリーズのまとめ記事をお楽しみに。