アーカイブ

第8章 9.マッシュアップとJavaScript

公開日:2007年6月28日

独立行政法人情報処理推進機構
セキュリティセンター

本ページの情報は2007年6月時点のものです。
記載の資料は資料公開当時のもので、現在は公開されていないものも含みます。

JavaScriptは、今日のWebアプリケーションにとって欠かせない存在となっている。

ブラウザ上で動作するJavaScriptコードからは、素早いレスポンスをユーザへ返すことができる。その背後でWebサーバと通信することも可能だ。ページのロードのたびに待たせることなく、滑らかな操作性を提供できる。いわゆるAjax手法である。

クライアント側マッシュアップではこのような、ブラウザ上のJavaScriptコードが大いに活躍することになる。また、主要なブラウザでサポートが進むHTML5においては、JavaScriptコードから呼び出せるAPIが大幅に増やされ、機能性が向上した。

これらのことから、ブラウザで動作するJavaScriptコードの規模と複雑さは増大しつつある。

ブラウザ以外のプラットフォームでJavaScript を動作させることも進んでいる。サーバプログラムやスタンドアロンアプリケーションの用途である。ブラウザ外環境におけるAPIの共有をはかるCommonJSという団体も、2009年から活動を始めた。

JavaScriptの長所と短所

JavaScriptの長所

  • JavaScriptは、シンプルかつ柔軟なプログラミング言語である。
  • 処理の単位は「関数」として記述する。関数はオブジェクトの一種であり、変数に代入して受け渡しが可能である。
  • オブジェクト指向プログラミング機能を備えている。インスタンスごとに振る舞いを定義することも、テンプレートを用いて一連のオブジェクトに共通の振る舞いをさせることも可能である。なお、クラスと呼ばれる存在は無い。
  • オブジェクトは、メソッド(「関数」の要素)やフィールド(「変数」の要素)を動的に追加でき、柔軟性に富む。
  • ひとつのオブジェクト全体をリテラル表記可能である。すなわち、あれこれと処理ロジックを書くことなく、静的にオブジェクトを定義できる。JSONデータフォーマットは、このリテラル表記の文法に基づいている。

JavaScriptの短所

  • データ型は存在しているが、JavaScriptには変数に関するデータ型の宣言が無い。変数に保持できる値を特定の型へ限定することができない。
  • グローバル変数の名前空間が単一である。複数のライブラリを併用する際に名前の衝突が起こるおそれがある。
  • 上記の性質があるため、プログラムの規模が大きくなるにつれ、誤りの混入のおそれも高くなる。

単一スレッドとハンドラ関数

ブラウザにおけるJavaScriptコードの多くは、単一スレッドの処理系を前提としたスタイルで書かれる。

単一スレッドの環境においては、瞬時には終わらないタスク、例えば、ネットワークアクセスや入出力等を開始したとき、呼び出し側のコードはその終了を待たないように書かれる。そして、タスク終了時に実行するコードを書いた関数を終了ハンドラとして登録しておく。

JavaScriptの制御はイベント処理機構に戻り、起動したタスクが終わるまでの間、ユーザからの新たな入力に反応する等の振る舞いができる。タスクが終了するとハンドラ関数が呼ばれ、そこでタスクに続く処理を行わせることができる。

HTML5においてWebWorker等のマルチスレッド技術が登場してはいるが、ハンドラ関数を用いるプログラミングスタイルは定着している。

ハンドラ関数への分割スタイルの課題

単一スレッドの処理系を前提としたプログラミングが行われる場合、ひとつづきの処理は複数のハンドラ関数に分割され、それらが連携動作するよう組み合わせられる。

それぞれの処理の断片の間でロジックの辻褄を合わせなくてはならないが、ここに誤りが混入しやすい。また、ハンドラ関数の処理内容に別のハンドラ関数の設定を記述することも、しばしば行われる。

コードは入り組んだものになるうえ、何がどのような順序で行われるか読み取りにくい。ソフトウェアの保守が困難になりがちである。

サーバ側JavaScript

JavaScriptは、サーバ側プログラムの記述にも使われはじめている。

ブラウザ外JavaScriptの処理系やライブラリの例 :

  • Rhino
  • Node.js

ブラウザ外JavaScriptの共通仕様:CommonJS

ブラウザにおけるJavaScriptの実行時ライブラリの基本部分は、windowオブジェクトを頂点とする定義済みオブジェクトと、それらに備わるプロパティや関数がDOM (Document Object Model) として定められている。

ブラウザ外JavaScriptの実行時ライブラリには当初、そのような共通の仕様が無かった。2009年以降、CommonJSという団体によるAPIの仕様策定の活動が始まる。ここには、30以上のJavaScriptライブラリ実装プロジェクトが参加している。

  • Common Node
  • Flusspferd
  • GPSEE
  • Narwhal
  • RingoJS
  • TeaJS
  • Wakanda 等

ただし、仕様の多くはまだ提案段階にあり、確定していない。2012年11月現在、状態がDRAFTやPROPOSALとなっているものが多くみられる。

CommonJSは当初、サーバ側JavaScriptのライブラリ仕様策定として始まった。現在ではサーバ側JavaScriptに限定されないものになっている。

仕様の中にはブラウザでサポートされているXMLHttpRequestオブジェクト等も提案されていて、近い将来、ブラウザ内JavaScriptとブラウザ外JavaScriptの間で共通のソースコードが使えるようになることも考えられる。

JavaScriptコードに対する制約メカニズム

ブラウザで動作するJavaScriptコードは、ブラウザがもつセキュリティ確保のための制約(同一源泉ポリシー)に沿って振る舞いが制限され、一定の保護効果が期待できる。

サーバ側JavaScriptの実行環境にこのような仕組みはない。ビジネスロジックのコードが一定の枠内で振る舞うよう制約を課す仕組みが必要なばあい、プログラム開発者自身の手で構築する必要がある。

ハンドラ関数のスパゲッティ

これまでJavaScriptプログラミングにおいては、ブラウザの単一スレッド環境の中でハンドラ関数を多用するスタイルが多く用いられてきた。しかし、このスタイルではソースコードの可読性を低下させてしまい、保守性を損なう。

例として、次のケースを考えてみる。「あるWebサーバからリソースの取得に成功したら別のWebサーバからもリソースを取得し、それが成功したらさらに第3のWebサーバからリソースを取得し、それらすべての組み合わせからひとつの結果を作りだす」、というロジックをJavaScriptで記述するとする。

この場合、HTTPリクエストを発行するとともに、成功もしくはエラーの応答を待ち受けるハンドラ関数を設置することになる。このうち、成功時のハンドラ関数の中では第2のWebサーバへのアクセスの開始とそれに伴うハンドラ関数の設置を記述し、そのうち成功時のハンドラ関数の中では第3のWebサーバへのアクセスを記述し...というように、設置したハンドラ関数の中で別のハンドラ関数を設置するという入れ子関係が複数段階におよんでしまう。

リスト1: 複数のWebアクセスを行う整理されていないコード例

//
// sequentialA - 3つのWebアクセスを順に行う関数
// (整理されていない形)
function sequentialA (uri1, uri2, uri3) {
  var xhr1 = new XMLHttpRequest ();
  xhr1.open ("GET", uri1, true);

  xhr1.onload = function (e) {
    if (this.status === 200) {
     var result1 = this.responseText;

     var xhr2 = new XMLHttpRequest ();
     xhr2.open ("GET", uri2, true);

     xhr2.onload = function (e) {
      if (this.status === 200) {
       var result2 = this.responseText;

       var xhr3 = new XMLHttpRequest ();
       xhr3.open ("GET", uri3, true);

       xhr3.onload = function (e) {
        if (this.status === 200) {
         var result3 = this.responseText;

         processAllResults ([result1, result2, result3]);
        } else {
         var reason3 = this.status + " - " + uri3;
         errorHandler (reason3);
        }
       }

       xhr3.onerror = function (e) {
        errorHandler (e);
       }

       xhr3.send ();
      } else {
       var reason2 = this.status + " - " + uri2
       errorHandler (reason2);
      }
     };

     xhr2.onerror = function (e) {
      errorHandler (e);
     }

     xhr2.send ();
   } else {
    var reason1 = this.status + " - " + uri1;
    errorHandler (reason1);
   }
  };

  xhr1.onerror = function (e) {
   errorHandler (e);
  }

  xhr1.send ();
}

連鎖の段数が多くなるにつれ、コードは入り組んだものになり、その保守は、より困難になる。

Promise

Promiseというプログラミングパターンがある。これは、非同期処理と、その事後処理とを仲介するものである。CommonJSにおいてもPromiseの仕様が提案されている。Promiseを用いると、非同期処理を起動するコードの中で事後処理の関数を具体的に参照する必要がなくなる。このことは、他への依存度の低い、すなわち再利用性の高いコード記述を可能にする。

非同期処理を複数連鎖させるとき、Promiseを導入することによって、シンプルに記述できる。ハンドラ登録のネストが深くなるような複雑なコードを書かずに済む。

順次処理のコード例

次は、CommonJSのPromises/A仕様を実装しているwhen.jsの使用例である。

リスト2: when.jsを用いた順次処理のコード例

//
// taskB - Webアクセスを行うタスク
// (Promise を使うスタイル)
function taskB (uri) {
  var myPromise = when.defer (); // 新たな 'Promise' を確保する

  var xhr = new XMLHttpRequest ();
  xhr.open ("GET", uri, true);

  xhr.onload = function (event) {
   if (this.status === 200) {
    var myResult = this.responseText;
    myPromise.resolve (myResult); // 'Promise' を「成功」で決着させ、結果を預ける
   } else {
    var myReason = this.status + " - " + uri;
    myPromise.reject (myReason); // 'Promise' を「失敗」で決着させる
   }
  };

  xhr.onerror = function (event) {
   var myReason = event.type + " - " + uri;
   myPromise.reject (myReason);  // 'Promise' を「失敗」で決着させる
  };
  xhr.send ();

  return myPromise;    // 未決着の 'Promise' を返す
}

//
// sequentialB - taskB 関数を使って3つのWebアクセスを順に実行
//
function sequentialB (uri1, uri2, uri3) {
  var list = [];

  taskB (uri1)
  .then (
   function (result) { list.push (result); return taskB (uri2); },
   null)
  .then (
   function (result) { list.push (result); return taskB (uri3); },
   null)
  .then (
   function (result) { list.push (result); processAllResults (list); },
   errorHandler);
}

taskBは、Webアクセスを1回のみ行う関数である。非同期でWebアクセスを起動するとともに、その内部で Promise と呼ばれる種類のオブジェクトを作り、関数の戻り値として返す。Promise オブジェクトには「未決着」と「決着」のふたつの段階がある。taskB関数から返される Promise オブジェクトは、「未決着」段階のものである。

起動したWebアクセスがその後成功するか失敗するかに応じ、taskB関数は、次のいずれかの方法で Promise オブジェクトを「決着」させる。

  • 「成功」による決着──Webアクセスが成功したとき、taskB関数の中で設定したハンドラがresolve ( ) メソッドを呼び出してmyPromise を「成功」の状態で決着させ、myPromiseに処理結果を預ける。
  • 「失敗」による決着──Webアクセスが失敗したとき、taskB関数の中で設定したハンドラがreject ( ) メソッドを呼び出してmyPromiseを「失敗」の状態で決着させ、myPromiseにはエラー情報を預ける。

taskBを呼び出す側のsequentialB関数では、返される Promise オブジェクトに対して then ( ) メソッドを適用し、成功時のハンドラ関数と失敗時のハンドラ関数を設定している。

上記のコード例ではthen ( ) メソッドの呼び出しを複数段階連鎖させている。then ( ) メソッドはさらに別の Promise オブジェクトを返してくるので、タスクの連鎖を記述できる。

この例では、連鎖の途中のthen ( ) の失敗時ハンドラ関数の指定を省略している(nullにしている)。このようにしておくと、どこかの段階で生じた「失敗」は、最後のthen ( ) まで伝播し、そこで指定されている失敗時ハンドラ関数が呼び出される。

並列処理の結果を集約する例

リスト3: 並列処理の結果をwhen.allを用いて集約するコード例

//
// parallelB - taskB 関数を使って3つのWebアクセスを並列に実行
//
function parallelB (uri1, uri2, uri3) {
  var myPromise1 = taskB (uri1);
  var myPromise2 = taskB (uri2);
  var myPromise3 = taskB (uri3);
  when.all (
   [myPromise1, myPromise2, myPromise3],
   processAllResults,
   errorHandler);
}

このように、Promiseを用いると成功・失敗の事象が発生する地点と、それらの事象に対処するロジックを記述する地点とを分離でき、モジュール性の高いコードを書くことができる。