なぜWebモジュールなのか?

このページでは、Web上のモジュールがなぜ有用なのか、そして今日Web上でモジュールを有効にするために使用できるメカニズムについて説明します。RequireJSで使用される特定の関数でラップされた形式に関する設計上の力については、別のページで説明しています。

問題点 § 1

  • WebサイトがWebアプリに変わってきている
  • サイトが大きくなるにつれてコードの複雑さが増す
  • 組み立てが難しくなる
  • 開発者は別々のJSファイル/モジュールを求めている
  • デプロイメントは1つまたは少数のHTTP呼び出しで最適化されたコードを求めている

解決策§ 2

フロントエンド開発者は以下のソリューションを必要としている

  • 何らかの#include/import/require
  • ネストされた依存関係を読み込む機能
  • 開発者にとって使いやすいが、デプロイメントを支援する最適化ツールによってバックアップされている

スクリプト読み込みAPI§ 3

最初に整理する必要があるのはスクリプト読み込みAPIです。候補をいくつか示します。

  • Dojo:dojo.require("some.module")
  • LABjs:$LAB.script("some/module.js")
  • CommonJS:require("some/module")

これらはすべてsome/path/some/module.jsの読み込みにマッピングされます。理想的には、時間が経つにつれてより一般的になる可能性があり、コードを再利用したいので、CommonJS構文を選択できます。

また、今日存在するプレーンなJavaScriptファイルを読み込むことを可能にする何らかの構文も必要です。開発者は、スクリプト読み込みの利点を得るために、すべてのJavaScriptを書き換える必要はありません。

ただし、ブラウザでうまく機能するものが必要です。CommonJS require()は同期呼び出しであり、モジュールをすぐに返すことが期待されています。これはブラウザではうまく機能しません。

非同期 vs 同期§ 4

この例は、ブラウザの基本的な問題を示す必要があります。Employeeオブジェクトがあり、ManagerオブジェクトをEmployeeオブジェクトから派生させたいとします。この例を参考に、スクリプト読み込みAPIを使用して次のようにコードを作成できます。

var Employee = require("types/Employee");

function Manager () {
    this.reports = [];
}

//Error if require call is async
Manager.prototype = new Employee();

上記のコメントが示すように、require()が非同期の場合、このコードは機能しません。ただし、ブラウザでスクリプトを同期的にロードすると、パフォーマンスが低下します。では、どうすればよいでしょうか?

スクリプト読み込み:XHR§ 5

XMLHttpRequest(XHR)を使用してスクリプトをロードすることを検討したくなるかもしれません。XHRを使用する場合、上記のテキストを修正できます。require()呼び出しを見つけるために正規表現を実行し、それらのスクリプトをロードし、eval()またはボディテキストがXHR経由でロードされたスクリプトのテキストに設定されているスクリプト要素を使用できます。

eval()を使用してモジュールを評価することは悪い

  • 開発者はeval()が悪いと教えられてきました。
  • 一部の環境ではeval()が許可されていません。
  • デバッグが困難になります。FirebugとWebKitのインスペクターには//@ sourceURL=の規則があり、evalされたテキストに名前を付けるのに役立ちますが、このサポートはブラウザー全体で普遍的ではありません。
  • evalコンテキストはブラウザによって異なります。IEでexecScriptを使用してこれを支援できるかもしれませんが、それはより多くの可動部分を意味します。

ボディテキストがファイルテキストに設定されたスクリプトタグを使用することは悪い

  • デバッグ中、エラーで取得する行番号は、元のソースファイルにマッピングされません。

XHRにはクロスドメインリクエストの問題もあります。一部のブラウザではクロスドメインXHRのサポートが提供されるようになりましたが、これは普遍的ではなく、IEではクロスドメイン呼び出し用に別のAPIオブジェクトXDomainRequestを作成することにしました。より多くの可動部分と、より多くの間違える可能性のあるもの。特に、非標準のHTTPヘッダーを送信しないように注意する必要があります。そうしないと、クロスドメインアクセスが許可されていることを確認するために、別の「プリフライト」リクエストが行われる可能性があります。

Dojoはeval()を使用したXHRベースのローダーを使用しており、機能しますが、開発者にとって不満の原因となっています。Dojoにはクロスドメインローダーがありますが、モジュールをビルドステップで変更して関数ラッパーを使用し、script src=""タグを使用してモジュールをロードする必要があります。開発者に負担をかける多くのエッジケースと可動部分があります。

新しいスクリプトローダーを作成する場合は、より良いものを実現できます。

スクリプト読み込み:Web Workers§ 6

Web Workersはスクリプトをロードする別の方法かもしれませんが

  • クロスブラウザサポートが強力ではありません
  • メッセージパッシングAPIであり、スクリプトはDOMと対話したい可能性が高いため、ワーカーを使用してスクリプトテキストを取得しますが、テキストをメインウィンドウに渡し、eval /スクリプトをテキストボディで利用してスクリプトを実行することを意味します。これには、上記で説明したXHRと同じ問題があります。

スクリプト読み込み:document.write()§ 7

document.write()を使用してスクリプトをロードできます。他のドメインからスクリプトをロードでき、ブラウザが通常スクリプトを消費する方法にマッピングされるため、簡単にデバッグできます。

ただし、非同期 vs 同期の例では、そのスクリプトを直接実行することはできません。理想的には、スクリプトを実行する前にrequire()の依存関係を知り、それらの依存関係が最初にロードされるようにすることができます。しかし、実行される前にスクリプトにアクセスすることはできません。

また、document.write()はページ読み込み後には機能しません。サイトの認識パフォーマンスを向上させるための優れた方法は、ユーザーが次のアクションに必要なときにオンデマンドでコードをロードすることです。

最後に、document.write()を介してロードされたスクリプトは、ページのレンダリングをブロックします。サイトの最高のパフォーマンスを実現しようとするとき、これは望ましくありません。

スクリプト読み込み:head.appendChild(script)§ 8

オンデマンドでスクリプトを作成し、それらをheadに追加できます

var head = document.getElementsByTagName('head')[0],
    script = document.createElement('script');

script.src = url;
head.appendChild(script);

上記のコードスニペットよりも少し複雑ですが、それが基本的な考え方です。このアプローチは、ページのレンダリングをブロックせず、ページ読み込み後でも機能するという点で、document.writeよりも優れています。

ただし、非同期 vs 同期の例の問題は残ります。理想的には、スクリプトを実行する前にrequire()の依存関係を知り、それらの依存関係が最初にロードされるようにすることができます。

関数ラッパー§ 9

したがって、依存関係を知り、スクリプトを実行する前に依存関係をロードする必要があります。それを行う最善の方法は、関数ラッパーを使用してモジュール読み込みAPIを構築することです。こんな感じです。

define(
    //The name of this module
    "types/Manager",

    //The array of dependencies
    ["types/Employee"],

    //The function to execute when all dependencies have loaded. The
    //arguments to this function are the array of dependencies mentioned
    //above.
    function (Employee) {
        function Manager () {
            this.reports = [];
        }

        //This will now work
        Manager.prototype = new Employee();

        //return the Manager constructor function so it can be used by
        //other modules.
        return Manager;
    }
);

そして、これがRequireJSで使用される構文です。モジュールを定義しないプレーンなJavaScriptファイルをロードしたいだけの場合は、簡略化された構文もあります。

require(["some/script.js"], function() {
    //This function is called after some/script.js has loaded.
});

このタイプの構文が選択されたのは、簡潔であり、ローダーがhead.appendChild(script)タイプのロードを使用できるようにするためです。

ブラウザでうまく機能する必要があるため、通常のCommonJS構文とは異なります。サーバープロセスがモジュールを関数ラッパーを持つトランスポート形式に変換する場合、通常のCommonJS構文をhead.appendChild(script)タイプのロードで使用できるという提案がありました。

コードを変換するためにランタイムサーバープロセスを使用することを強制しないことが重要だと考えています

  • デバッグが奇妙になります。サーバーが関数ラッパーを挿入しているため、行番号がソースファイルとずれます。
  • より多くのギアが必要です。フロントエンド開発は静的ファイルで可能である必要があります。

非同期モジュール定義(AMD)と呼ばれるこの関数ラッピング形式の設計上の力とユースケースの詳細については、なぜAMDなのか?ページをご覧ください。