PHP で Javascript を動的結合

Strategic Web Design : ITpro の連載記事「技術者視点のユーザビリティ考」の第30回 JavaScriptの動作を軽くするための工夫で紹介されていた動的に js ファイルを統合する仕組み Supercharged Javascript が、中々興味深い。
要は指定された js ファイルへのアクセスを PHP に送り、結合して送り返すのだが、これが中々良さそうだ。
多少、修正して当サイトにも実装してみたので、自分用にメモ。
時間が有るときに WordPress 用のプラグインとして利用できるようにしてリリースするかも。

js ファイルの動的結合

まずは、js ファイルを結合する PHP。
元記事のままだと、別ディレクトリの js ファイルを指定できなさそうなので、以下のように修正した。

<?php
   $cache = dirname($_SERVER&#91;'SCRIPT_FILENAME'&#93;).'/cache';        // $cache is cache dir

   ini_set("zlib.output_compression", "Off");                     // Disable zlib compression, if present, for duration of this script.
   header("Content-Type: text/javascript; charset=UTF-8");        // Set the content type header
   header("Cache-Control: must-revalidate");                      // Set the cache control header (http 1.1 browsers MUST revalidate -- always)
   header('Expires: '.gmdate("D, d M Y H:i:s", time() + (60 * 60 * 24 * 365)).' GMT'); // Set the Expires header (1 year)

   // Here we are going to extract the filename list.
   if (isset($_GET&#91;'files'&#93;)) {
     $fileList = str_replace(" ", "", trim(urldecode($_GET&#91;'files'&#93;)));
   } else {
     $expl = explode("/",$HTTP_SERVER_VARS&#91;"REQUEST_URI"&#93;);
     $fileList = trim(urldecode($expl&#91;count($expl)-1&#93;));
   }
   $orgFileNames = explode(",",$fileList);						// $fileNames now is an array of the requested file names.

   // Go through each of the files and get its last modified time so we
   // can send a last-modified header so caching works properly
   $newestFile = 0;
   $ii=0;
   $longFilename = ''; // This is generated for the Hash
   $fileNames = Array();
   for ($i=0; ($i < count($orgFileNames)); $i++) {
      $orgFileNames&#91;$i&#93; = trim($orgFileNames&#91;$i&#93;);    // Get rid of whitespace
      if (preg_match('/^\/.*\.js$/i', $orgFileNames&#91;$i&#93;)) {
        $orgFileNames&#91;$i&#93; = dirname($_SERVER&#91;'SCRIPT_FILENAME'&#93;).$orgFileNames&#91;$i&#93;;
      }

      if (preg_match('/\.js$/i',$orgFileNames&#91;$i&#93;)) { // Allow only files ending in .js in the list.
         $fileNames&#91;$ii++&#93;=$orgFileNames&#91;$i&#93;;         // Valid file name, so go ahead and use it.
         $longFilename .= $orgFileNames&#91;$i&#93;;          // Build our LONG file name for the hash.
         $lastMod = @filemtime($orgFileNames&#91;$i&#93;);    // Get file last modified time
         if ($lastMod > $newestFile) {                // Is this the newest file?
            $newestFile = $lastMod;                   // Yup, so mark it.
         }
      }
   }

   // Begin *BROWSER* Cache Control
   //
   $fileHash = md5($longFilename);       // This generates a key from the collective file names
   $hash = $fileHash . '-'.$newestFile;  // This appends the newest file date to the key.

   // Get all the headers the browser sent us.
   if (function_exists('getallheaders')) {
      $headers = getallheaders();
   } elseif (function_exists('apache_response_headers')) {
      $headers = apache_response_headers();
   }
   if (ereg($hash, $headers['If-None-Match']))  {   // Look for a hash match
      header('Last-Modified: '.gmdate('D, d M Y H:i:s', $newestFile).' GMT', true, 304);
      die();
   }

   // We are still alive so save the hash+latest modified time in the e-tag.
   header("ETag: \"{$hash}\"");

   // If there's no change we'll send a cache control header and die.
   if (isset($headers['If-Modified-Since'])) {
      if ($newestFile <= strtotime($headers&#91;'If-Modified-Since'&#93;)) {
         // No change so send a 304 header and terminate
          header('Last-Modified: '.gmdate('D, d M Y H:i:s', $newestFile).' GMT', true, 304);
          die();
       }
   }

   // Set the last modified date as the date of the NEWEST file in the list.
   header('Last-Modified: '.gmdate('D, d M Y H:i:s', $newestFile).' GMT');

   // Begin File System Cache Control
   //
   $fp = @fopen("$cache/$fileHash.txt","r");
   if ($fp) {
      if ($newestFile>@filemtime("$cache/$fileHash.txt")) { fclose($fp); $fp=false;}
   }
   if (!$fp) {
      $buffer='';
      for ($i=0; ($i < count($fileNames)); $i++) {
         $buffer .= @file_get_contents($fileNames&#91;$i&#93;)."\n\n";
      }

      // We've created our concatenated file so first we'll save it as
      // plain text for non gzip enabled browsers.
      $fp = @fopen("$cache/$fileHash.txt","w");
      @fwrite($fp,$buffer);
      @fclose($fp);

      // Now we'll compress the file (maximum compression) and save
      // the compressed version.
      $fp = @fopen("$cache/$fileHash.gz","w");
      $buffer = gzencode($buffer, 9, FORCE_GZIP);
      @fwrite($fp,$buffer);
      @fclose($fp);
   }

   // Begin Output
   //
   if (strstr($_SERVER&#91;'HTTP_ACCEPT_ENCODING'&#93;, 'gzip')) {
      // Browser can handle gzip data so send it the gzip version.
      header ("Content-Encoding: gzip");
      header ("Content-Length: " . filesize("$cache/$fileHash.gz"));
      readfile("$cache/$fileHash.gz");
   } else {
      // Browser can not handle gzip so send it plain text version.
      header ("Content-Length: " . filesize("$cache/$fileHash.txt"));
      readfile("$cache/$fileHash.txt");
   }
?>

これに scripts.php とでも名前をつけて保存しておきましょう。

.htaccess は、こんな感じで修正しておけばおっけ。

<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteCond %{REQUEST_URI} ",+.*\.js$"
RewriteRule . /scripts.php?files=%{REQUEST_URI} [L]
</IfModule>

これで、以下のようにカンマ区切りで js ファイルを列挙してやれば指定された js ファイルをすべて結合した一つのファイルとして返してくれます。

<script type="text/javascript" src="/wp-includes/js/jquery/jquery-1.2.2.min.js, /wp-includes/js/jquery/jquery.plugins.js, /wp-includes/js/quick-comments.js"></script>

WordPress への応用

さて WordPress では標準の javascript ロード基準が無いため、各プラグインが各々好きなように javascript をセットしています。
※調べたら、Wordpress 用の javascript ローダーがありました。
 wp_enqueue_scriptで外部JavaScriptの読み込みをスマートに at WordPress.ex-libris.jp

そんな訳で、読み込んでる javascript ファイルが半端無い数になって重くなってしまっているサイトもあります。
自動で対応できると嬉しいので、ウチでは以下のようにしました。

修正するのは、使用しているテーマの中の functions.php
以下の記述を追加すれば、上手いこと行くはず。

<?php
function _start() {
 ob_start();
}

function _end_flush() {
 $content = ob_get_contents();
 ob_end_clean();

 $content = preg_replace('/<!--&#91;^>]*-->/i', '', preg_replace('/[\r\n]/i', '', $content));
 $content = preg_replace('/(<&#91;^>]*\/>)/i', "\n".'$1'."\n", $content);
 $content = preg_replace('/(<script&#91;^>]*>.*?<\/script>)/i', "\n".'$1'."\n", $content);

 $contents = explode("\n", $content);
 $re_siteurl = '/^'.preg_quote(preg_replace('/^(https?:\/\/[^\/]*\/)/', '$1', get_settings('siteurl').'/'), '/').'/i';

 $inner_js = '';
 $outer_js = '';
 $inline_js = '';

 for ($i=0; ($i < count($contents)); $i++) {
  $contents&#91;$i&#93; = trim($contents&#91;$i&#93;);
  if ($contents&#91;$i&#93; != '') {

   if (preg_match('/<script &#91;^>]*><\/script>/i', $contents[$i])) {
    $outer = true;

    preg_match_all('/(src|type|charset)=["\'](.*?)["\']/i', $contents[$i], $out, PREG_SET_ORDER);
    for ($j=0; ($j < count($out)); $j++) {
     if (strtolower($out&#91;$j&#93;&#91;1&#93;) == 'src') {
      $out&#91;$j&#93;&#91;2&#93; = preg_replace('/^'.preg_quote(get_settings('siteurl').'/', '/').'(.*\.js)&#91;\?&#93;?&#91;^\?&#93;*$/i', '/$1', $out&#91;$j&#93;&#91;2&#93;);
      if (!preg_match('/^https?:\/\//i', $out&#91;$j&#93;&#91;2&#93;) && preg_match('/\.js$/i', $out&#91;$j&#93;&#91;2&#93;)) {
       if ($inner_js != '') {$inner_js .= ", ";}
       $inner_js .= $out&#91;$j&#93;&#91;2&#93;;
       $outer = false;
      }
     }
    }
    unset($out);

    if ($outer) {$outer_js .= $contents&#91;$i&#93;."\n";}

   } elseif (preg_match_all('/<script &#91;^>]*>(.*?)<\/script>/i', $contents[$i], $out, PREG_SET_ORDER)) {
    $inline_js .= $out[0][1]."\n";

   } else {
    echo $contents[$i]."\n";
   }
  }
 }

 if ($inner_js != '') {
  echo '<script type="text/javascript" src="'.$inner_js.'"></script>'."\n";
 }
 if ($outer_js != '') {
  echo $outer_js;
 }
 if ($inline_js != '') {
  echo '<script type="text/javascript">//<!&#91;CDATA&#91;'."\n";
  echo $inline_js;
  echo '// &#93;&#93;></script>'."\n";
 }

}

add_action('wp_head', '_start', 0);
add_action('wp_head', '_end_flush', 100000);
add_action('wp_footer', '_start', 0);
add_action('wp_footer', '_end_flush', 100000);
?>

どんな感じになるかは、当サイトのソースでも眺めてみてください。

30 thoughts on “PHP で Javascript を動的結合

  1. をかもと 投稿作成者

    バニラさん、どもです。

    さくらのスタンダードプランでもダメでしたか。うーん。
    ちょっと時間のある時にじっくりと調べてみますね。

    返信
  2. バニラ

    こんばんは。
    さくらのスタンダードプランで試してみましたが、ダメでした。
    “.*\.js$” と js を1つにするとキャッシュは作成されました。
    js を複数にすると作成されないですね。
    www ルートに scripts.php を設置しています。

    こちらのやり方が何かまずい気がします。
    別のアプローチで試してみます。
    ありがとうございました。

    返信
  3. をかもと 投稿作成者

    バニラさん、どもです。

    をかもとさんの環境は さくらのレンタルサーバ ですよね?

    このサイトは、さくらのスタンダードプランで動作しています。
    PHP は CGI モードで動作しています。

    さくらを試されるのでしたら、「さくらのレンタルサーバ非公式FAQ ( http://faq.sakuratan.com/ ) 」は、結構役立つ情報がまとめてありますよ。
    そちらも、参照してみてください。

    返信
  4. バニラ

    間違ったこと書いていました。

    さくらのレンタルサーバもビジネスプロ以外はセーフモードで動いていると思うのですが

    ビジネスプロ以外はモジュールモードですね、すいません。

    返信
  5. バニラ

    お返事ありがとうございます。
    教えて頂いたURIは、一応チェックしています。

    をかもとさんの環境は さくらのレンタルサーバ ですよね?
    さくらのレンタルサーバもビジネスプロ以外はセーフモードで動いていると思うのですが
    どのプランで運用しているか教えて頂いてもよろしいでしょうか?
    お試し期間でxreaとどう違うのがテストしてみたいと考えています。
    よろしくお願いします。

    XAMPPのローカル環境でも、ファイルがリクエストされていませんでした。
    htaccessの設定ではなくて、php.ini や httpd.conf の設定かもしれません。

    返信

コメントを残す

メールアドレスが公開されることはありません。

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください