14
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

JSエコシステムぶらり探訪(6): AMDとモジュールローダー

Last updated at Posted at 2020-11-15

第2回から第5回にかけては、Node.jsの伝統的なモジュールシステムであるCommonJSに絞って挙動を説明していました。これらはECMAScript 2015で導入されたES Modulesで置き換えられつつありますが、それ以外にもいくつかのモジュールシステムが提案され、運用されてきました。これらのうちAMD, UMD, SystemJSについて本稿で扱います。

←前 目次

モジュールローダー

モジュールシステムをWebブラウザでも使うための方法として、モジュールバンドラーを前回紹介しました。

モジュールバンドラーが依存関係をビルド時に解決する一方、依存関係をクライアントで実行時に解決する方法 (モジュールローダー) も古くから試みられていました。1 Node.jsは依存関係を実行時に解決するので、ある意味ではこちらのほうが自然な方法に見えます。ここでCommonJSがそのまま使えたら便利そうですが、CommonJSフォーマットをそのまま使う方法は、以下の3つの問題を抱えています。

  • 1ファイル1モジュールの制限
  • 関数ラッパーが必要
  • require の解析が必要

1ファイル1モジュール

CommonJSは1つのファイルに1つのモジュールという対応関係があります。これはサーバー環境であればそれほど問題ではありませんが、クライアント環境では各スクリプトを個別に転送することになります。特にHTTPレイヤが十分に最適化されていなければ大きな通信コストが発生しかねません。

必要に応じて (たとえばパッケージ単位で) 複数のモジュールをひとつのファイルにまとめて転送できたほうが望ましいでしょう。

関数ラッパーが必要

第2回で説明したように、CommonJSのモジュールファイルはグローバル文脈でそのまま実行されるのではなく、モジュール文脈で実行されます。 var/function はグローバルを汚染しませんし、 this はモジュールを指しています。

ブラウザの <script> タグはグローバル文脈で実行される2ので、CommonJSファイルをそのまま <script> に入れることはできません。かわりに、 XMLHttpRequestやfetchで自力でスクリプトファイルを取得し、自力で加工する必要があります。

require の解析が必要

第1回で説明したように、ブラウザ上での通信は非同期で行うのが普通で、同期的な通信APIには様々な問題や制限があります。 require 関数は同期的に動作することが期待されているので、その中で非同期にスクリプトを読みに行くのでは間に合いません。

そのため、CommonJSモジュールのソースコードをそのまま使うのであれば、Webpackがそうしているように require(定数) という形の呼び出しをあらかじめ発見して、モジュールコードの実行前に依存先モジュールの取得を済ませておく必要があります。

AMD

こうしたCommonJS Modulesとモジュールローダーの相性問題を解決するために生まれたのがAMD (Asynchronous Module Definition) というモジュールフォーマットです。元はCommonJSプロジェクトのTransport/Cという規格提案としてスタートしたものが、CommonJSプロジェクトから離脱して今に至るようです。

たとえば、以下のようなCommonJS Modulesプログラムを考えます。

multiply.js
exports.multiply = function(x, y) {
  return x * y;
};
square.js
var multiply = require("./multiply");
exports.square = function(x) {
  return multiply.multiply(x, x);
};
main.js
var square = require("./square");
console.log(square.square(42));

これはAMDでは以下のようになります。

multiply.js
define("multiply", [], function() {
  //   ^^^^^^^^^^  ^^-- 依存関係
  //            \------ モジュール名
  return {
    multiply: function(x, y) {
      return x * y;
    },
  };
});
square.js
define("square", ["multiply"], function(multiply) {
  //   ^^^^^^^^  ^^^^^^^^^^^^           ^^^^^^^^- ロードされた依存関係 (順番に渡される)
  //          \             \-------------------- 依存関係
  //           \--------------------------------- モジュール名
  return {
    square: function(x) {
      return multiply.multiply(x, x);
    },
  }
});
main.js
define("main", ["square"], function(square) {
  console.log(square.square(42));
  return {};
});

第一引数(モジュール名)と第二引数(依存関係) は存在しない場合は省略できます。

AMDはCommonJSと違い、「必要な依存関係があらかじめわかっていること」と「必要な依存関係が先に読み込まれている」という制約がつきます。そのため、AMDのモジュールはCommonJSにおける冠頭形モジュールに対応するといえます。

CommonJS Modules互換モード

CommonJS Modulesとの相互運用性のために、他にも以下のモードが定められています。これらを使うとCommonJS Modulesのコードをほぼそのまま関数で包むだけでAMDとして使えるようになります。

square.js
// require, exports, moduleという名前の依存関係を指定した場合は特別に、CommonJS互換のrequire/exports/moduleオブジェクトが与えられるようになる。
// このrequireはあらかじめロードされた依存関係を取り出す処理のみを行う。
define("square", ["require", "exports", "module", "multiply"], function(require, exports, module) {
  var multiply = require("./multiply");
  exports.square = function(x) {
    return multiply.multiply(x, x);
  };
});
square.js
// 依存関係の自動発見を行うモード。
// 関数ソースコード (Function.prototype.toString) から require() 呼び出しがスキャンされる。
// 依存関係が空のときにスキャンが行われるが、全てのAMDローダーがサポートしているわけではない。
// なお、依存関係が空のときは ["require", "exports", "module"] を指定したのと同等の引数が渡ってくることが規定されている。
define("square", function(require, exports, module) {
  var multiply = require("./multiply");
  exports.square = function(x) {
    return multiply.multiply(x, x);
  };
});

モジュールの評価順序

CommonJS Modulesではモジュールの評価は最初に require が呼ばれたタイミングで行われます。

main.js
console.log("Loading dep...");
const dep = require("dep");
console.log("Answer is " + dep.answer);
dep.js
console.log("This is dep");
exports.answer = 42;

これを実行すると以下の順序で出力されます。

Loading dep...
This is dep
Answer is 42

WebpackをはじめとするCommonJSモジュールバンドラーはこのような require の挙動をシミュレートするため、同じ結果が期待できます。

一方、AMDは読み込み済みの依存関係が与えられる形式で、 require 関数も読み込み済みの依存関係を返す処理しか行いません。そのため、必要になってからモジュールの評価を行うということは (require.ensure を使わない限り) できません。CommonJS互換機能を使った場合でも、実際の挙動はAMD特有のものに揃えられます。

main.js
define("main", function(require, exports, module) {
  console.log("Loading dep...");
  var dep = require("./dep");
  console.log("Answer is " + dep.answer);
});
dep.js
define("dep", [], function() {
  console.log("This is dep");
  return {
    answer: 42,
  };
});
This is dep
Loading dep...
Answer is 42

モジュール名

AMDのモジュール名はCommonJS Modulesと同様にファイルシステムと対応しています。つまり、 foo/bar のようにスラッシュ区切りで指定することで階層性を表すすることができます。

AMDには、 require や依存関係の指定が相対パスであることだけ規定されています。拡張子の省略に関する規定や、 非相対パス (node_modules からの探索) の規定は存在しません。

RequireJS

RequireJSはAMDの主要な実装です。

RequireJSを利用したプロジェクトの構成は色々考えられますが、一番シンプルなのはモジュール名=パスとして対応づける方法です。先ほどの main.js, square.js, multiply.js を含んだ、以下のようなディレクトリ構成を考えます。

|- index.html
|- require.js
|- main.js
|- square.js
\- multiply.js
index.html
<!doctype html>
<html>
  <head>
    <script data-main="main.js" src="require.js"></script>
  </head>
</html>

require.js はRequireJSのWebサイトで配布されているものを配置します。

これを実行すると、ブラウザは直接的には require.js のみをロードし、残りのスクリプトはRequireJSによって依存解決したうえで読み込まれます。

UMD

ここまでで既に以下の3種類のモジュールシステムが登場しています。

  • グローバルモジュール (IIFE + window に直接エクスポート)
  • CommonJS Modules
  • AMD

3種類のモジュールシステムがあるということは、各ライブラリは必要に応じて個別に「グローバルモジュール」「CommonJS」「AMD」をサポートする必要があるということです。3種類の形式でそれぞれ配布してもよいのですが、CommonJSとAMDは構文上はグローバルJSと互換性があるため、自力でモジュール種別を判別するライブラリを作ることができます。これをパターン化し規格化したものがUMD (Universal Module Definition) です。

UMDも要件によって微妙に異なる形態をとることができ、templatesディレクトリにその書き方がまとめられています。たとえばcommonjsStrictは以下のような形式です。

// 全体がIIFEになっているので、デフォルトではグローバルを汚染しないようになっている
(function (root, factory) {
  if (typeof define === 'function' && define.amd) {
    // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AMDは define.amd オブジェクトを定義するよう規定している
    // AMDの場合はAMDのdefine関数を呼び出す。exportsが必要なのでそれも要求しておく
    define(['exports', 'dependency1', 'dependency2'], factory);
  } else if (typeof exports === 'object' && typeof exports.nodeName !== 'string') {
    //       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ CommonJS文脈ではexportsが定義されているのでそれで判定している
    // AMDをサポートするために、依存関係の取得はファクトリ関数の外側で行うようになっている。
    // そのため、CommonJSを要求された場合はここでrequireをすることになる
    factory(exports, require('dependency1'), require('dependency2'));
  } else {
    // AMDでもCommonJSでもない場合はグローバル変数を定義する
    // 依存関係もグローバル変数にあると仮定する
    factory((root.myLibrary = {}), root.dependency1, root.dependency2);
  }
}(typeof self !== 'undefined' ? self : this, function (exports, dependency1, dependency2) {
  // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ グローバルオブジェクトの検出。IIFE内ではthisが変わってしまうのでここで書く
  //                                                            ^^^^^^^^^^^^^^^^^^^^^^^^ インポートはAMD形式
  // CommonJS形式での定義とエクスポートをここに書く
  exports.something = function() {}
}));

UNPKG

UNPKG はnpmにアップロードされたUMDパッケージを配信するサーバーです。scriptタグやRequireJSを使って直接読み込むために使うことができます。

bower

npmが元々Node.jsで動かすためのJavaScriptプログラムを配布するパッケージマネージャとして始まったのに対して、ブラウザで動かすためのJavaScriptプログラムを配布するパッケージマネージャとして作られたのが bower です。

現在ではNode.js・ブラウザの如何を問わずnpmで管理するほうが主流であり、bowerは自身を非推奨としてnpmへの移行を呼びかけています。

SystemJSとSystem.register

SystemJSはECMAScript Modules (ES Modules; ESM) をサポートしたライブラリベースのモジュールローダーです。Webブラウザ、Node.jsともにES Modulesのネイティブ実装が存在していますが、それらが提供されていないバージョンでもESMの恩恵を受けられることがSystemJSの特徴のようです。

ESMはそれまでのJavaScriptにない構文を使うため、ESMの互換レイヤとしてSystem.registerというモジュールフォーマットが定義されています。これはAMDとよく似ていますが、ES Modulesに特有のセマンティックスに対応しているようです。AMDと同様、複数のモジュールを1ファイルにまとめるトランスポートフォーマットとしての役割も担っているようです。

jspm

jspmは歴史的に2種類に分けられます。

  • 旧jspm (jspm-cli) はbowerのようなパッケージマネージャで、2020年6月に非推奨化されました。
  • 新jspm (jspm.dev) は旧jspmにかわり2020年6月にリリースされたサービスです。これはUNPKGと同様、npmのパッケージの配布サーバーとして提供されていますが、UMDではなくESMをベースにしています。

まとめ

  • モジュールフォーマット
    • AMDはクライアント側でのモジュールローダーを実装するにあたってのCommonJSの問題を解消したモジュールフォーマット。
    • UMDはグローバルモジュール、CommonJSモジュール、AMDのいずれとしても読み込めるように書かれたモジュールと、そのパターン。
    • System.registerはAMDと似ているが、ES Modulesのセマンティックスに沿って設計されている。
  • モジュールローダー
    • RequireJSはAMDの主要なローダー。
    • SystemJSはSystem.registerの主要なローダー。
  • パッケージ配布サーバー
    • UNPKGはUMD形式のためのCDN。
    • jspm.devはES Modules形式のためのCDN。
  • npm互換以外のパッケージマネージャー
    • bower。非推奨。
    • jspm-cli。非推奨。

←前 目次

  1. RequireJSのような動的な解決とWebpackのようなビルド時解決のどちらが良いかには様々な議論があるようです。本稿ではこの比較には深入りしません。

  2. ES Modulesについては別記事で扱う予定

14
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?