JavaScript Module Systems Showdown: CommonJS vs AMD vs ES2015

JavaScript 開発がますます一般的になるにつれて、名前空間と依存関係を扱うのがはるかに難しくなっています。 この問題に対処するために、モジュール システムという形でさまざまなソリューションが開発されました。 この投稿では、開発者が現在採用しているさまざまなソリューションと、それらが解決しようとする問題を探ります。 続きを読む!

Introduction: なぜ JavaScript モジュールが必要なのか。

他の開発プラットフォームに精通している場合、カプセル化と依存性の概念について、おそらく何らかの概念をお持ちだと思います。 異なるソフトウェアの断片は、通常、以前に存在したソフトウェアの断片によって何らかの要件を満たす必要があるまで、分離して開発されます。 他のソフトウェアがプロジェクトに組み込まれた時点で、そのソフトウェアと新しいコードの部分との間に依存関係が生まれます。 これらのソフトウェアが一緒に動作する必要があるため、ソフトウェア間でコンフリクトが発生しないようにすることが重要です。 これは些細なことに聞こえるかもしれませんが、ある種のカプセル化を行わなければ、2つのモジュールが互いに衝突するのは時間の問題なのです。 これは、C ライブラリ内の要素が通常プレフィックスを持つ理由の 1 つです。 言い換えれば、コードの任意のブロックが実行される時点で依存関係が満たされていることを確認するのは、開発者の仕事です。 また、開発者は、依存関係が正しい順序で満たされていることを確認する必要があります (特定のライブラリの要件)。次の例は、Backbone.js の例の一部です。 スクリプトは正しい順序で手動で読み込まれる:

<!DOCTYPE html><html lang="en"> <head> <meta charset="utf-8"> <title>Backbone.js Todos</title> <link rel="stylesheet" href="todos.css"/> </head> <body> <script src="../../test/vendor/json2.js"></script> <script src="../../test/vendor/jquery.js"></script> <script src="../../test/vendor/underscore.js"></script> <script src="../../backbone.js"></script> <script src="../backbone.localStorage.js"></script> <script src="todos.js"></script> </body> <!-- (...) --></html>

JavaScript 開発がますます複雑になるにつれ、依存関係の管理は面倒になることがあります。 ロード チェーンの適切な順序を維持するために、より新しい依存関係をどこに置くべきでしょうか。 これらは、成長し続ける JavaScript の状況に対応する必要性から生まれました。

An Ad Hoc Solution: 異なるソリューションがテーブルに何をもたらすか見てみましょう。 Revealing Module Pattern

Most module systems are relatively recent. この例は、Addy Osmani の JavaScript Design Patterns book から引用しました。

(少なくとも ES2015 で let が登場するまでの)JavaScript スコープは、関数レベルで機能します。 言い換えれば、関数の内部で宣言されたどのようなバインディングもそのスコープから逃れることはできません。 このため、(他の多くの JavaScript パターンと同様に) 公開モジュール パターンは、プライベート コンテンツをカプセル化するために関数に依存しています。 他のすべての宣言は、それらを囲む関数スコープによって保護されます。 var を使用し、プライベート スコープを囲む関数への即時呼び出しを行う必要はありません。 依存性の問題についてはあまり効果がありません。 適切なモジュール システムでは、この問題にも対処しようとします。

Pros

  • どこでも実装できるほどシンプル (ライブラリなし、言語サポートなし)。

Cons

  • プログラムでモジュールをインポートする方法がない (eval を使用する場合を除く)。
  • 依存関係は手動で処理する必要がある。
  • モジュールの非同期ロードはできない。
  • 円形の依存関係は厄介である。
  • 静的コード解析器では解析しにくい。

CommonJS

CommonJS は、サーバーサイド JavaScript アプリケーションの開発を支援するための一連の仕様を定義することを目的としたプロジェクトである。 CommonJS チームが取り組もうとしている領域の 1 つは、モジュールです。 Node.jsの開発者は、もともとCommonJSの仕様に従うつもりでしたが、後にそれを断念しました。 モジュールに関しては、Node.js の実装は非常に影響を受けています。

// In circle.jsconst PI = Math.PI;exports.area = (r) => PI * r * r;exports.circumference = (r) => 2 * PI * r;// In some fileconst circle = require('./circle.js');console.log( `The area of a circle of radius 4 is ${circle.area(4)}`);

ある晩、Joyent で、ひどいアイデアだとわかっている機能に対するおかしな要求に少しいらいらしていると言ったとき、彼は私に「CommonJS は忘れろ」といいました。 あれはもうだめだ。 我々はサーバサイドJavaScriptなのだから」と。 – NPM クリエイターの Isaac Z. Schlueter が Node.js クリエイターの Ryan Dahl

を引用しています。

Node.js のモジュールと CommonJS の間のギャップを埋めるライブラリの形で Node.js のモジュール システム上に抽象化されたものが存在します。 この投稿の目的では、ほとんど同じである基本的な機能のみを示します。

Node と CommonJS の両方のモジュールでは、モジュール システムと相互作用するために本質的に 2 つの要素があります。 requireexports です。 require は、他のモジュールから現在のスコープにシンボルをインポートするために使用できる関数です。 requireに渡されるパラメータはモジュールのidである。 Node の実装では、node_modules ディレクトリにあるモジュールの名前 (そのディレクトリにない場合は、そのパス) である。 exportsは特殊なオブジェクトで、この中に入れたものはすべてpublic要素としてエクスポートされます。 フィールドの名前は保存されます。 NodeとCommonJSの独特な違いは、module.exportsオブジェクトの形にあります。 Nodeでは、module.exportsはエクスポートされる本当の特別なオブジェクトで、exportsはデフォルトでmodule.exportsにバインドされる変数に過ぎません。 一方、CommonJSはmodule.exportsオブジェクトを持ちません。 実用的な意味は、Node では module.exports:

// This won't work, replacing exports entirely breaks the binding to// modules.exports.exports = (width) => { return { area: () => width * width };}// This works as expected.module.exports = (width) => { return { area: () => width * width };}

CommonJS のモジュールはサーバー開発を念頭に置いて設計されているため、完全に事前構成されたオブジェクトをエクスポートすることは不可能だということです。 当然ながら、APIは同期的である。 言い換えれば、モジュールはソースファイル内で必要とされる瞬間と順序でロードされます。

Pros

  • Simple: 開発者はドキュメントを見ることなくコンセプトを把握できます。
  • 依存性管理が統合されている: モジュールは他のモジュールを必要とし、必要な順序でロードされる。
  • require はどこでも呼び出せる: モジュールはプログラムによってロードできる。

Cons

  • Synchronous API は特定の用途 (クライアントサイド) には適さない。
  • 1 つのモジュールに 1 ファイル。
  • Browsers にはローダーライブラリかトランスパイルが必要。
  • モジュールのコンストラクタ関数がない (Node はサポートしているが).
  • 静的コード解析器では解析しにくい.

実装

すでにひとつの実装について (部分的に) 話しましたが、その実装は以下のとおりです。 Node.js.

Node.js JavaScript Modules

クライアントについては、現在、webpack と browserify という 2 つの一般的なオプションがあります。 Browserify は、Node のようなモジュール定義を解析し (多くの Node パッケージはこれですぐに動作します!)、自分のコードとそれらのモジュールからのコードを、すべての依存関係を運ぶ単一のファイルにバンドルするために明示的に開発されました。 一方、Webpackは、公開前にソース変換の複雑なパイプラインを作成することを処理するために開発されました。 これには、CommonJS モジュールのバンドルも含まれます。

Asynchronous Module Definition (AMD)

AMD は、CommonJS が採用した方向性に不満を持つ開発者グループから生まれました。 実際、AMD は開発の初期に CommonJS から分割されました。 AMD と CommonJS の主な違いは、非同期モジュール読み込みのサポートにあります。

//Calling define with a dependency array and a factory functiondefine(, function (dep1, dep2) { //Define the module value by returning a value. return function () {};});// Or:define(function (require) { var dep1 = require('dep1'), dep2 = require('dep2'); return function () {};});

非同期読み込みは、JavaScript の伝統的なクロージャ イディオムを使用することにより実現されます。 モジュール定義とモジュールのインポートは同じ関数によって運ばれます: モジュールが定義されるとき、その依存性は明示されます。 そのため、AMD ローダーは実行時に与えられたプロジェクトの依存関係グラフの全体像を把握することができます。 したがって、ロード時に互いに依存しないライブラリーを同時にロードすることができます。 これは、スタートアップ時間が良好なユーザー エクスペリエンスに不可欠であるブラウザーにとって特に重要です。

Pros

  • Asynchronous loading (より良いスタートアップ時間)。
  • require および exports の互換性。
  • 依存性管理の完全統合。
  • 必要に応じて、モジュールを複数のファイルに分割可能。

Cons

  • 構文的にやや複雑。
  • トランスパイルしない限りローダーライブラリが必要。
  • 静的コード解析器での解析が難しい。

実装

現在、最も人気のある AMD の実装は require.AMD です。js と Dojo です。

Require.js for JavaScript Modules

require.js を使用することは非常に簡単です: HTML ファイルにライブラリをインクルードし、data-main 属性を使用してどのモジュールを最初に読み込むべきかを require.js に指示します。 Dojo にも同様の設定があります。

ES2015 Modules

幸い、JavaScript の標準化を支える ECMA チームが、モジュールの問題に取り組むことを決定しました。 その結果は、JavaScript 標準の最新リリースで見ることができます。 ECMAScript 2015 (以前は ECMAScript 6 として知られていました) です。

//------ lib.js ------export const sqrt = Math.sqrt;export function square(x) { return x * x;}export function diag(x, y) { return sqrt(square(x) + square(y));}//------ main.js ------import { square, diag } from 'lib';console.log(square(11)); // 121console.log(diag(4, 3)); // 5

Example taken from Axel Rauschmayer blog

The import directive can be used to bring modules into the namespace.Type 1, Type 2, Type 3, Type 4, Type 5, Type 6, Type 7, Type 8, Type 9, Type 10, Type 10, Type 10, Type 10, Type 10, Type 10, Type 10 の各指令は、モジュールを名前空間に取り込むのに使用できます。 このディレクティブは、requiredefine とは対照的に、動的ではありません (すなわち、任意の場所で呼び出すことはできません)。 一方、export ディレクティブは明示的に要素を公開するために使用できます。

importexport ディレクティブの静的な性質により、静的解析器はコードを実行せずに依存関係の完全なツリーを構築することができます。 ES2015 はモジュールの動的ロードをサポートしています。

System.import('some_module') .then(some_module => { // Use some_module }) .catch(error => { // ... });

実際には、ES2015 は動的および静的モジュール ローダーの構文のみを指定しています。 実際には、ES2015 の実装はこれらのディレクティブをパースした後に何もする必要がありません。 System.js などのモジュール ローダーは、次の ECMAScript 仕様がリリースされるまで必要です。

このソリューションは、言語と統合されていることにより、ランタイムがモジュールに対して最適なロード戦略を選択できるようにします。 言い換えれば、非同期ロードが利益をもたらすとき、ランタイムがそれを使用することができます。

Pros

  • Synchronous and asynchronous loading supported.
  • Syntactically simple.
  • Support for static analysis tools.
  • Itegrated with the language (eventually supported everywhere, no need for libraries).Syntactous and asynchronous loading supported.
  • Circular dependencies supported.

Cons

  • Still not supported everywhere.

Implementations

The major JavaScript runtime none supports ES2015 modules in their current stable branches.Unknown the current stable branch.Unknown the stable branch. これは、Firefox、Chrome、または Node.js のサポートがないことを意味します。 幸いなことに、多くのトランスパイラがモジュールをサポートしており、ポリフィルも利用可能です。 現在、Babel の ES2015 プリセットは問題なくモジュールを処理できます。

Babel for JavaScript Modules

The All-in-One Solution: System.js

1 つのモジュール システムを使用して、レガシー コードから移行しようとしている自分に気づくかもしれません。 あるいは、何が起こっても、選択したソリューションがまだ動作することを確認したいかもしれません。 System.js の登場です。CommonJS、AMD、および ES2015 モジュールをサポートするユニバーサル モジュール ローダーです。 BabelやTraceurのようなトランスパイラと一緒に動作し、NodeとIE8+の環境をサポートします。 System.js はすべての作業をオンザフライで行うため、ES2015 モジュールの使用は、一般に、実稼働モードでの構築ステップ中にトランスパイラーに任されるべきです。 実稼働モードでないときは、System.js はトランスパイラーを呼び出して、実稼働環境とデバッグ環境間のシームレスな移行を提供できます。 Auth0 で使用しているもの

Auth0では、JavaScriptを多用しています。 サーバー側のコードには、CommonJS スタイルの Node.js モジュールを使用しています。 クライアントサイドのコードには、AMDを使用しています。 React ベースのパスワードなしロック ライブラリでは、ES2015 モジュールを選択しました。 サインアップして、今すぐプロジェクトで Auth0 を使い始めましょう。

あなたは開発者で、私たちのコードが好きですか? もしそうなら、今すぐエンジニアのポジションに応募してください。 素晴らしいチームがあります!

結論

モジュールを構築し、依存関係を処理することは、過去に面倒なことでした。 ライブラリや ES2015 モジュールのような新しいソリューションにより、ほとんどの苦痛が取り除かれました。 新しいモジュールやプロジェクトの開始を検討している場合、ES2015 は正しい方法です。 常にサポートされますし、トランスパイラやポリフィルを使った現在のサポートも優れています。 一方、プレーンなES5コードにこだわりたい場合は、クライアントにはAMD、サーバーにはCommonJS/Nodeという分担が通常の選択として残されています。 あなたの感想を下のコメント欄に書き込むのを忘れないでください。 ハックオン!

コメントする