Jan 8, 2005

「条件付きGET」のススメ

一般にApacheに代表されるHTTP 1.1サーバーは、Webブラウザが通常のHTMLファイルにアクセスした時に、Last-Modified(更新時刻)ヘッダとETag(更新時刻などから生成されたハッシュ値)ヘッダを返します。次回以降のアクセスでは、この両ヘッダにセットされた更新時刻やハッシュ値が異なる場合だけコンテンツのダウンロードを行い、そうでない場合にはローカルキャッシュを参照することでトラフィックを削減できます(Shift+リロードなどの特定の操作をした場合には無条件でGETされます)。この機能は「条件付きGET(Conditional GET)」と呼ばれており、RFC2068: Hypertext Transfer Protocol -- HTTP/1.1などに動作が規定されています。一方、PHPファイルなどの動的コンテンツにアクセスした時には、上記の両ヘッダを返されないため、そのままでは条件付きGETが行われません。

さて、Movable Type(に限りませんが)でファイルの拡張子を.phpにしている例は結構見かけますが、この「条件付きGET」に配慮している例はあまり見かけません。Webブラウザはこれらのサイトに対して毎回無条件にGETを行っており、無駄にトラフィックやサーバー資源を消費し、レスポンス時間も犠牲にしているはずです。また、Last-Modifiedヘッダを返さないサイトはSEO的にも不利になる可能性があります。なぜならある程度クレバーなクローラーならば、サーバー負荷を抑えるために動的コンテンツの収集を控え目に行うロジックが含まれているはずだからです。

無条件にGETすることに意味がある場合もあります。例えば、PHPで書かれたアクセスカウンターなどのように毎回異なる結果を生成すべき場合がそうです。しかし、再構築時間の短縮を目的として単にモジュール化しているだけのことであれば、無条件にGETすることによるメリットはありません。

そんなわけで、このエントリーではSimon Willison: Supporting Conditional GET in PHPを参考にして、PHPファイルに対して「条件付きGET」を有効にするための方法を述べます。ここで述べているのはスタティック・ページに対する方法です。ダイナミック・パブリッシング(デフォルトではすべて無条件GET)で条件付きGETを有効にするのはもう少しスマートにできますから、興味のある方はダイナミック・パブリッシング: 条件付きリクエストを参照してみてください。

単純なケース

そもそもLast-Modifiedヘッダを返すわけですから、「更新日時」を何らかの方法で決定する必要があります。

まず、一番簡単なケースとして「そのPHPファイルの更新日時」を「更新日時」とする場合を考えます。この場合、以下のコードをファイルの先頭に記述することで条件付きGETが実現できます。doConditionalGetの定義は上記のSimon Willisonのものをわずかに変更してあります。

注意: 「<?php」の前に(空白や改行を含め)文字を入力してはいけません。ファイルの「先頭」に記述することは「必須」です。
<?php
$ts = getlastmod();
doConditionalGet($ts);
 
function doConditionalGet($timestamp) {
  // A PHP implementation of conditional get, see 
  //   http://fishbowl.pastiche.org/archives/001132.html
  $last_modified = gmdate('D, d M Y H:i:s T', $timestamp);
  $etag = '"'.md5($last_modified).'"';
  // Send the headers
  header("Last-Modified: $last_modified");
  header("ETag: $etag");
  // See if the client has provided the required headers
  $if_modified_since = isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) ?
    stripslashes($_SERVER['HTTP_IF_MODIFIED_SINCE']) :
    false;
  $if_none_match = isset($_SERVER['HTTP_IF_NONE_MATCH']) ?
    stripslashes($_SERVER['HTTP_IF_NONE_MATCH']) : 
    false;
  if (!$if_modified_since && !$if_none_match) {
    return;
  }
  // At least one of the headers is there - check them
  if ($if_none_match && $if_none_match != $etag) {
    return; // etag is there but doesn't match
  }
  if ($if_modified_since && $if_modified_since != $last_modified) {
    return; // if-modified-since is there but doesn't match
  }
  // Nothing has changed since their last request - serve a 304 and exit
  header('HTTP/1.1 304 Not Modified');
  exit;
}
?>

少し複雑なケース

一つのページをモジュール化して複数のファイルに分割し、それを「<?php readfile("..."); ?>」などしてインクルードしている場合には、上に示したコードでは不十分です。

例えば、サイドバー部分をモジュール化してファイルに書き出し、メインのPHPファイルではそれを読み込んでいる場合を考えます。そうすると上記のコードではメインのPHPファイルが更新されたときにしか、「更新日時」が更新されません。メインコンテンツに比較してサイドバーの更新頻度が高い場合には(おそらくこれがモジュール化した理由と推察されるわけですが)これでは不都合があるでしょう。

どちらかと言えば、メインのPHPファイルの更新日時とサイドバー部分の更新日時のうち新しい方をこのPHPファイルの「更新日時」とした方が合理的です。これは、上記のコードの先頭部分を以下のように書き換えることで実現できます。「<$MTBlogSitePath$>left-column.php」、「<$MTBlogSitePath$>right-column.php」は、インクルードしているファイル名に適宜読み替えてください。

<?php
$ts = getlastmod();
$ts_list[] = getlastmod();
$ts_list[] = filemtime('<$MTBlogSitePath$>left-column.php');
$ts_list[] = filemtime('<$MTBlogSitePath$>right-column.php');
sort($ts_list, SORT_NUMERIC);
$ts = array_pop($ts_list);
doConditionalGet($ts);

もっと複雑なケース

…は、考えていません。だいたい上の2つの例で分かったかと思いますが、doConditionalGetに与える「$ts」というタイムスタンプ変数に更新日時となる値をセットすればよいのです。

About Me

My Photo

つくばで働く研究者

Total Pageviews

Amazon

Copyright 2012 Ogawa::Buzz | Powered by Blogger
Design by Web2feel | Blogger Template by NewBloggerThemes.com