次の方法で共有


トラフィックのリダイレクト: IIS 4.0 による効果的な Custom 404 メッセージ

George Young
Development Lead
Microsoft Corporation

March 30, 1999
日本語版最終更新日 1999 年 10 月 19 日

MSDN Online Voices の新たに登場したコラム "Code Corner" にようこそ! Code Corner では、しばしば直面する問題に焦点を当てたサンプル コード、開発者によく寄せられる機能面での要求、あるいは私がことさらに感心した事柄を重点的に取り上げる予定です。この新しい MSDN Online サイトのコラムは、(この記事もそうですが)多かれ少なかれ、最先端の開発を担当してきた私の経験に基づくものです

カスタム エラー メッセージ

コーナーの口火を切るこのコラムでは Microsoft Internet Information Server (IIS) 4.0 の Custom 404 エラー ページに関する開発と展開について一通り見てみましょう。カスタム エラー メッセージは IIS 4.0 の新機能です。これにより、特定のサーバー エラーが発生したときに、カスタマイズしたページを指定して表示できるようになります。Custom 404 は、訪問者がサーバー上に存在しないファイルを要求したときに発生する、おなじみの File Not Found エラーを処理します。的外れの URL 要求を手際よく処理することは、あらゆる場面で素晴らしいことです。大量のコンテンツを移動したり、サーバーやクラスタを切り換えるような場合には(最近私たちが行った Site Builder Network (SBN) と MSDN Online の統合などはその典型です)、なおさら重要です。

訪問者により快適な体験をしてもらうために、プレーンな HTML ページを作って、親切なエラー メッセージや印象に残る効果を持たせるのはそれほど難しいことではありません。サーバーが出す 404 エラー メッセージよりこの方がましであることは確かです。しかし、このエラー メッセージにほんの少し手を加えるだけで、つまりリソースのマッピングを行ったり、スクリプトを書くことで、ファイルやディレクトリへの期限切れのパスを新しい有効なパスにマッピングして、ユーザーを目的のファイルや、目次、サイト マップ、検索ページといった便利なリソースに直接案内することができます。

一夜にして 7,000 ページを移動

SBN と MSDN Online の統合作業で、SBN サイトの 7,000 あまりのファイルがメインのクラスタである microsoft.com から msdn.microsoft.com クラスタに移動しました。このとき移動後のファイルがすべて同じパス名を使えたら(サーバー名だけは変更しなければなりませんが)、サーバー名のマッピングだけでことは済むため、リダイレクト作業は大幅に簡略化できたはずです!

サイトの構成を変更する作業はたいてい大掛かりなものになりますが、私たちのマッピング作業も例に違わず一筋縄ではいきませんでした。たとえば、Web Workshop (前の SBN Workshop)など一部の領域は、同じパス名を使用できましたが、Voices (前の SBN Magazine を母体に拡張した領域。今あなたが訪れている場所がそうです)をはじめとする他の領域は、ファイル名はそのまま使用できましたが、ホストするディレクトリは現在別のものになっています。ごくわずかですが、完全に消滅した領域もあります。また、一部のファイルの名前を変更した領域もあります。

次の表で、我々の 404 シナリオの主だった 5つの概略を知ることができます。

シナリオ 旧パスの例 新パスの例
1つのファイルを新しいファイルに置き換え /sitebuilder/whatsnew.asp /siteguide/recent.asp
領域全体を消滅 /gallery/stylesheets/ --
パスを変更 /sitebuilder/siteinfo/glossary/ /workshop/essentials/glossary/
旧ディレクトリを1つのファイルで処理 /sbnmember/wms/ /osig/wm/default.asp
直接にマッピング /xml/ /xml/

ものごとを集中化しておくこと

このようにさまざまなエラーが起こりうる条件下では、ファイルを旧サーバーに要求する大勢の訪問者を手際よく処理することが非常に重要になります。その解決策の 1 つが、旧サーバー上のすべてのディレクトリにリダイレクト用のページ (Response.Redirect() 呼び出しを持つ .asp ページか、または META Refresh ヘッダーを使用する .htm ページ) を置く方法でした。この方法は、コード数は少なくて済みますが、すべてのディレクトリにファイルが物理的に存在しなければならないという厄介な問題が残ります。この方法では、膨大な数の独立したファイルを追跡しなければならないため、保守や展開に大変な労力を必要とします。

2つ目の解決策は、Custom 404 ページをサーバー上に登録し、スクリプトとマッピングによる方法ですべてのリダイレクトをこの 1つのファイルで処理しようという考えです。これは、マッピングのあらゆる可能性を残らずつぶしていくという、より戦略的な作業を必要としますが、私たちはこの方法を採ることにしました。これは方法としては面白い試みであり、すべてのリダイレクト情報を 1つの場所の 1つのファイルで管理することができるため、MSDN Online のような大規模な Web サイトには願ったりの方法です。

かくして、私たちは、サーバーとクライアントの両方で一連のパスとファイル名をスクリプトを使ってマッピングする作業に取りかかるはめになりなりました。サーバー上では、Web Workshop での NavPath リンクの作成など、JScript の配列を使ってパスをマッピングした確かな経験があるので、最初にその方法を用いることにしました。しかもこの方法には(サーバーの場合)、ブラウザの機能(やバージョンのマイナーなバグ)に気を遣うことなく、最近気に入っている正規表現などのスクリプト エンジンの最新機能を利用できるメリットもあります。

残念ながら、起動用の一部のレガシーな フレームセットに関するコードは削除できなかったので、マッピング コードをサーバー上に置くことを断念しました。古いフレームセット コードでは、フレームセットで使用するコンテンツ ドキュメントの区切りに "#" を使っており(従来のオフライン表示方法による制約です)、この "location.hash" 情報はサーバーに渡されません。このことは、私たちのフレームセットを含む要求はサーバー上では構文解析できないことを意味します。この場面でできることは、サイト全体を俯瞰するホーム ページにユーザーを案内するぐらいです。これでは、効果的なリダイレクトとはとてもいえません。

そこで、私たちは、ターゲット ブラウザのユーザーに、JScript 対応ブラウザでもエラーが起きないような快適な体験を満喫していただけるよう、スクリプトによるソリューションをクライアント サイドで構築することに意を決しました。スクリプトの作成にあたっては、できるだけ多くのブラウザを効率的に扱えるように「最大公約数」的な方法をとりました。さらに、特定のブラウザを無視しなければならないケースも 2、3ありました。

アーキテクチャ

単一のリダイレクト ページの作成にあたっては、次の 3つの基本事柄を必要としました。

  • 最小限のパス マップ数をメモリに格納する
  • 要求されたURLを解析し、新しい有効なURLにマッピングする
  • マッピングによる一致が得られた場合には、一定の説明文とリンクを書き出す

パス マップを格納する

新旧のパスを対応させたパス フラグメント マッピング情報を格納するために、パイプで区切った文字列配列を使用することにしました。JScript の連想配列を使えばもっとすっきりした解決になっていたかも知れませんが、パフォーマンスが犠牲になるためこの方法は採りませんでした。コードをサーバーに高速に移動できる構造にしたかったのです。マッピング要素の1つ1つがプロパティとなる JScript オブジェクトも使おうと思えば使えました。

私たちはマッピングを、古いパス、新しいパス、およびパス識別子の3つのタイプに分けました。次に配列要素について、個別性の最も高い情報を含む要素が最初に処理されるように配列要素に順序を付けました。URL は、最も個別性の高い情報の一致が得られるまで(あるいは一致するものがないとわかるまで)フィルタを使って配列を絞り込みます。下の略記されたパスの配列を見てもわかるように、/siteinfo/ 旧ディレクトリにあった newtosite.asp ページ (現在の /siteguide/using.asp) に関する個別情報は、/siteinfo/ (/siteguide/ に対応) 内のその他すべてのファイルに関するより一般的な情報よりも優先して処理されます。

                  
var aRedir = new Array  // old path | new path | path id
(
  // ファイル マッピング - 個別ファイル名
  "/sitebuilder/whatsnew.asp|/siteguide/recent.asp|gde",
  "/sitebuilder/siteinfo/newtosite.asp|/siteguide/using.asp|gde",

  // 消滅領域 - もう存在しない領域
  "/sbnmember/promote/||mbr",
  "/sitebuilder/tour/||tur",

  //ディレクトリ マッピング - 最も一般的。全ディレクトリが対象
  "/sitebuilder/siteinfo/glossary/|/workshop/essentials/glossary/|gls",
  "/sitebuilder/siteinfo/|/siteguide/|gde"
);

古いパスと新しいパスを突き合わせする

配列構造の準備ができた後、私たちは、URL を補足し、aRedirs 配列の中で情報の一致があるか(ないかを)調べ、一致していれば該当する旧パス情報を新パス情報に置き換える対の関数を書きました。最初の関数 GetRedirIndex() は、旧パスに突き当たるまでひたすら aRedirs 配列を調べ、一致するものがあればその添字を返します。この関数はもう 1つの GetNewUrl() 関数に組み入れることもできましたが、あえて分離しました。こうすることで、コードがよりわかりやすくなり、複数の場所で添字を使用するよう場合にも修正しやすいと考えたからです。GetNewUrl() は、一致した配列要素を取り出して解析し、適切なリンク情報やテキスト情報を書き出すのに必要な 3つのマッピング要素を生成します。

                  
function GetRedirIndex(sUrl)
{
  // sReqUrl が一致するまで aRedir を繰り返す
  for (var i=0;i<aRedir.length;i++)
  {
    var sRedir = aRedir[i];
    // 一致したら、配列の添字を返す
    if (-1 != sUrl.indexOf(sRedir.substring(0,sRedir.indexOf("|"))))
    {
      return i;
    }
  }
  // 一致しなければ、-1 (false) を返す
  return -1;
}

function GetNewUrl(sUrl)
{
  // 一致した配列要素の添字を取得する
  var iIndex = GetRedirIndex(sUrl);
  if (-1 != iIndex)
  {
    // 受け取った添字が有効だったので、sOldPath と sNewPath を解析する
    var sRedir = aRedir[iIndex];
    var sOldPath = sRedir.substring(0,sRedir.indexOf("|"));
    var sNewPath = sRedir.substring(sOldPath.length + 1);
    sNewPath = sNewPath.substring(0,sNewPath.indexOf("|"));

    // sNewPath がスラッシュで終わっていたら、sOldPath とスワップする
    if ("/" == sNewPath.substring(sNewPath.length-1)) 
    {
      return (sUrl.substring(0,sUrl.indexOf(sOldPath)) 
      + sNewPath + sUrl.substring((sUrl.indexOf(sOldPath) 
      + sOldPath.length),sUrl.length));
    }

    // sNewPath が空でなかったら、それはページということになるので、
    // sOldPath の先頭に追加する
    else if ("" != sNewPath) 
    {
      return (sUrl.substring(0,sUrl.indexOf(sOldPath)) + sNewPath );
    }

    // sNewPath が空白の場合には、"Ex-Area" が返される
    else 
    {
      window.sExArea = sRedir.substring((sOldPath.length + 1) + (sNewPath.length + 1));
    }
  }

  // 受け取った添字が有効でなかった(一致がなかった)ので、開放し、false を返す
  else return false;
}

GetNewUrl() は、2、3のことを行う下のコードからインラインで呼び出されます。このコードは、その要求がフレームセット(実際には我々のレガシー コードにのみ関連するフレーム セットですが)に対するものであったかどうかを、location.href を使って、具体的には location.href を解析して location.pathname および location.hash と等価なものを取得する方法で判定します。これは、クライアント サイド スクリプティングの違いの影響が出た 1つの例です。私たちは、Array オブジェクトをサポートしていない Navigator 2 は考慮しないことにしました。また、location.hash をスクリプト エンジンに公開していない Internet Explorer 3 も、特殊なケースでは無視することにしました。

                  
var sReqUrl = location.href;

var sReqUrl = sReqUrl.substring(5); // '404;' を切り落とす
sReqUrl = sReqUrl.substring(7); // 'http://' を切り落とす
sReqUrl = sReqUrl.substring(sReqUrl.indexOf("/"));  // サーバー名を切り落とす

var bIsFramed = false;
if (-1 != sReqUrl.indexOf("c-frame.htm")) bIsFramed = true;

// ベースの sNewUrl を設定し、変数を false に初期化する

var sNewUrl = "https://msdn.microsoft.com/";
var sNewPath = false; 
var bMatched = false; 

// sReqUrlがフレームセットである場合には、正しい sNewPath を取得する
// (IE3 に特別なケースで、NN2 を回避する)

if (!bIsNN2)
{
  if (bIsFramed)
  {
    if (bIsIE3) 
    {
      sNewPath = sReqUrl.substring(0,sReqUrl.substring(1).indexOf("/") + 2);
    }
    else 
    {
      sNewPath = GetNewUrl(sReqUrl.substring(sReqUrl.indexOf("#") + 1));
    }
  }

  // そうでなければ、非フレームセットのsNewPathを取得する

  else sNewPath = GetNewUrl(sReqUrl);

  // 一致したら、bMatchedをtrueに設定する

  if (sNewPath) 
  {
    bMatched = true;
    sNewUrl += sNewPath;
  }
} 

一致した場合、その情報に基づいて適切なリンクとテキストを書き出す

最後に、一致した結果(あるいは一致しなかったことで)識別された適正なリンク情報を document.write() メソッドを使ってブラウザ ウィンドウに書き出します。ここではコード例は紹介しません。実装に強く依存したコードになるからです。実際にどんな具合になるかは、3月30日(火)の午後にスタートする古い Site Builder Network サイトで何か不適切な URL を要求することで確認できます。たとえば、かつての SBN Magazine (現在の MSDN Online Voices) の古い URL を使ってみてください。

そこで目にするものが私たちの Custom 404です。私たちは、スクリプトをバージョン 3 およびそれ以降のバージョンのすべてのブラウザで動作させるために、いくつかの試練を乗り越える必要がありました。サーバー上で ASP を使ってコーディングすることの利点をまざまざと思い知らされたわけですが、この Custom 404 については、よくできた安定したソリューションだと思っています。

クライアントとサーバーではどのぐらいの違いがあるか 2、3 事例を見てみましょう。

サーバー上で処理を行う場合の相違点

私たちがサーバー サイド スクリプトを選んでいたとしても、ロジックやマッピング配列に関しては、その格納先がアプリケーションのスコープ変数になったであろうことを除けば(変数への格納には LookupTable Object が使用されることになったでしょう)、さほど違いがなかったでしょう。実際、このコードは、document.write() ステートメントの部分を ASP の Response.Write() に変更すれば、後はそのまま IIS 4.0 上に持ち込むことができます。ただし、もしサーバーだったら、少し違ったやり方をしたと思われる点はいくつかあります。

代入と評価

もしサーバーだったら、オブジェクト変数の宣言と変数への代入を行ってから、別のステートメントの中でそれを評価するようなことはしないで(このように2回に分けた処理したのは、Navigator 4.03 を使っているクライアントにスクリプトのバグがあったからです)、代入と評価を 1つのステートメントで実行したでしょう。

                  
sNewPath = GetNewUrl(sTopicUrl);
if (sNewPath) 
{
  bMatched = true;
  sNewUrl += sNewPath;
}

この部分が次のように変わります。

                  
if (sNewPath = GetNewUrl(sTopicUrl)) 
{
  bMatched = true;
  sNewUrl += sNewPath;
}

String.replace() と Array.split() の使用

replace() メソッドを使用すれば、indexOf()substring() で文字列を作成するよりも入力の手間を少なからず省くことができ、コードの可読性も高まります。次の例に見られるように、私たちはこのメソッドを頻繁に使用しています。ここでは、パスの配列の中で一致するものが見つかったときに、新しいパス フラグメントが古いパス フラグメントに置き換わります。

                  
return (sUrl.substring(0,sUrl.indexOf(sOldPath)) 
  + sNewPath + sUrl.substring((sUrl.indexOf(sOldPath) 
  + sOldPath.length),sUrl.length));

このコードをreplace()メソッドを使うと次のようになります。

                  
return sUrl.replace(sOldPath,sNewPath);

クライアント上では使用できなかった **Array.split() を使えるため、コードの見た目もいくらかすっきりします。たとえば次のコードは、

                  
var sRedir = aRedir[iIndex];
var sOldPath = sRedir.substring(0,sRedir.indexOf("|"));
var sNewPath = sRedir.substring(sOldPath.length + 1);
sNewPath = sNewPath.substring(0,sNewPath.indexOf("|"));
window.sExArea = sRedir.substring((sOldPath.length + 1) + (sNewPath.length + 1));

次のように見た目もすっきりします。

                  
var aRedirData = aRedir[iIndex].split();
var sOldPath = aRedirData[0];
var sNewPath = aRedirData[1];
window.sExArea = aRedirData[2];

Response.Redirect() の使用

私たちは、2つの理由からクライアント上で中間的なページを使用しました。1つは、MSDN Online にリダイレクトする前に、Site Builder Network を親しくご利用くださっている訪問者の方々にサイト統合の案内情報を提供したいと考えたからです。そして、もう1つは、クライアント サイドの多くのリダイレクト スクリプトが煩わされている恐怖の戻りボタンのループを避けたいと考えたからです。location.href = sNewUrl を使用した場合には、訪問者にとって戻りボタンは実質的に使う意味がなくなります。これは、リダイレクト ページもデスティネーション ページもどちらもブラウザの履歴に記録されるからです。ユーザーが戻りボタンをクリックすると、リダイレクト ページがヒットされ、直ちにその先のページに案内されます。location.replace( sNewUrl ) メソッドは、デスティネーション ページだけをヒストリに保持することでこの問題を解決しましたが、これは Internet Explorer および Navigator 4 またはそれ以上のバージョンしかサポートしていません。

サーバー上の Response.Redirect(sNewUrl) では、戻りボタンの問題は解決されます。おそらく新規サイトの場合、最初の数週間は過渡的な「スプラッシュ」ページを使うことになるでしょうが、その後は、通常透過的な Response.Redirect() ソリューションに移行することになるでしょう。

George Young は、Internet Explorer チームの開発者であり、以前は Windows 2000、MSDN Online、および Site Builder Network サイトの開発を手がけました。余暇には、Windows Media Player でメキシコのラジオ局に耳を傾け、ニューオーリンズからワシントン州レドモンドへキャデラックで通勤しています。