CSS,JSファイルの統合と縮小化プログラムの使い方

今回は、CSS,JSファイルの統合と縮小化するプログラムの使い方を紹介します

有名どころのプラグインとして Head Cleaner 等が有りますが、機能が多すぎるせいかソースコードを見てもなかなか理解するのは大変です

今回は、シンプルに head, footer 毎の CSS, JS の統合と縮小化のみなのでコード量も少なめです

概要

機能

  • 読み込み順を CSS, JS の順に並べ替えます
  • theme, plugin の CSS の統合
  • theme, plugin の jS の統合
  • theme, plugin の CSS内の画像URL base64 変換

統合したファイルは、キャッシュファイルにして transient api を用いて扱います

また、下記機能には対応していません 低リスクの安全指向です (^^)

  • 外部サイトの CSS, JS は統合、縮小化しません
  • WordPressコアの CSS, JS は統合、縮小化しません
  • インラインの CSS, JS に対しては縮小化しません
  • HTML部は縮小化しません
  • js を head部からfooter部へ移動する機能はありません
  • zip圧縮機能はありません

プラグインインストール

この機能は、celtispack プラグインをインストールして CSS JS Minify モジュールを有効化すると使用できるようになります

ダウンロードは、WordPress Plugin : Celtispack ページから行うことが出来ます

使用ライブラリ

minify

Minify

CSS, JS を縮小化するプログラムがいくつか入っています

この中の以下の3つのファイルを使用します

  • JSMin.php – modified PHP implementation of Douglas Crockford’s JSMin.
  • UriRewriter.php – Rewrite file-relative URIs as root-relative in CSS files
  • CSSmin.php – PHP port of the CSS minification tool distributed with YUICompressor

プログラム

ヘッダー、フッター部

wp_head(), wp_footer() にフックして出力するCSS,JS を対象とします

下記のようにしてフックしたデータを加工するためにスタート部の優先度を高く、エンド部の優先度を低くして、この間の優先度のフック処理を捕捉します

    // wp_head 出力フィルタリング・ハンドラ追加
    add_action( 'wp_head', array($this, 'wp_head_buffer_start'), 1 );
    add_action( 'wp_head', array($this, 'wp_head_buffer_end'), 9999 );
    // wp_footer 出力フィルタリング・ハンドラ追加
    add_action( 'wp_footer', array($this, 'wp_footer_buffer_start'), 1 );
    add_action( 'wp_footer', array($this, 'wp_footer_buffer_end'), 9999 );

フックするスタート部で ob_start() を実行して、エンド部で ob_end_flush() を実行します

    /*
    * バッファリング開始
    */
    public function wp_head_buffer_start() {
      ob_start( array(&$this, 'wp_head_minify') );
    }
    public function wp_footer_buffer_start() {
      ob_start( array(&$this, 'wp_footer_minify') );
    }

    /*
    * バッファリング終了
    */
    public function wp_head_buffer_end() {
      ob_end_flush();
    }        
    public function wp_footer_buffer_end() {
      ob_end_flush();
    }        

    /*
    * フィルター
    */
    public function wp_head_minify($contents) {
        return self::minify($contents, 'head');
    }
    public function wp_footer_minify($contents) {
        return self::minify($contents, 'footer');
    }

ob_start の引数に指定した関数で、wp_head(), wp_footer() にフックした様々なデータをバッファリングして、minify 関数で加工してから出力することができます

CSS,JS 統合と縮小

結合と縮小を行うメインプログラムです

    public function minify($contents, $type) {
        $options = $this->options;

        //ホーム、投稿、固定ページの場合のみ並び替え統合結果をページ毎キャッシュで高速化
        if(is_home() || is_singular()){
            $pgid = md5('celtispack_'. $_SERVER['REQUEST_URI']);
            $cache = get_transient( $pgid );
            if ( !is_array($cache) ){
                $cache = array();
            }
            elseif ( !empty( $cache['css_js_minify']['content'])){
                if ($cache['css_js_minify']['md5'] !== md5($contents. $options['minify'])){
                    unset($cache['css_js_minify']);
                } else {
                    if(!empty($cache['css_js_minify']['jsfile']) &&  ! file_exists( $cache['css_js_minify']['jsfile'] )){
                        unset($cache['css_js_minify']);
                    }
                    if(!empty($cache['css_js_minify']['cssfile']) && ! file_exists( $cache['css_js_minify']['cssfile'] )){
                        unset($cache['css_js_minify']);
                    }
                    if(!empty( $cache['css_js_minify']['content']) ) {
                        return $cache['css_js_minify']['content'];
                    }
                }
            }
        }
        else {
            $pgid = false;
        }

        $jsfile  = array('infile' =>array(), 'outfile' =>array(), 'inline' =>array(), 'key' => '');
        $cssfile = array('infile' =>array(), 'outfile' =>array(), 'inline' =>array(), 'key' => '');
        //JS
        $infileurl = get_template_directory_uri() . '|' . get_stylesheet_directory_uri(). '|' . plugins_url();
        $spcontent = Celtis_lib::htmltagsplit_keyword($contents, '<script', '/script>',  array('javascript', 'src=', $infileurl, '.js'), array());
        if(isset($spcontent[1])){
            $max = count($spcontent);
            for($n=1; $n <$max; $n++ ){
                if(false === in_array($spcontent[$n], $jsfile['infile'])){
                    $jsfile['infile'][] = $spcontent[$n];
                    $jsfile['key'] .= $spcontent[$n];
                }
            }
        }
        $spcontent = Celtis_lib::htmltagsplit_keyword($spcontent[0], '<script', '/script>',  array('javascript', 'src='), array());
        if(isset($spcontent[1])){
            $max = count($spcontent);
            for($n=1; $n <$max; $n++ ){
                if(false === in_array($spcontent[$n], $jsfile['outfile'])){
                    $jsfile['outfile'][] = $spcontent[$n];
                }
            }
        }
        $spcontent = Celtis_lib::htmltagsplit($spcontent[0], '<script', '/script>' );
        if(isset($spcontent[1])){
            $max = count($spcontent);
            for($n=1; $n <$max; $n++ ){
                $jsfile['inline'][] = $spcontent[$n];
            }
        }
        //CSS
        $infileurl = plugins_url();
        $spcontent = Celtis_lib::htmltagsplit_keyword($spcontent[0], '<link', '/>',  array('stylesheet', 'href=', $infileurl, '.css'), array());
        if(isset($spcontent[1])){
            $max = count($spcontent);
            for($n=1; $n <$max; $n++ ){
                if(false === in_array($spcontent[$n], $cssfile['infile'])){
                    $cssfile['infile'][] = $spcontent[$n];
                    $cssfile['key'] .= $spcontent[$n];
                }
            }
        }
        $infileurl = get_stylesheet_directory_uri();
        $spcontent = Celtis_lib::htmltagsplit_keyword($spcontent[0], '<link', '/>',  array('stylesheet', 'href=', $infileurl, 'style', '.css'), array());
        if(isset($spcontent[1])){
            $max = count($spcontent);
            for($n=1; $n <$max; $n++ ){
                if(false === in_array($spcontent[$n], $cssfile['infile'])){
                    $cssfile['infile'][] = $spcontent[$n];
                    $cssfile['key'] .= $spcontent[$n];
                }
            }
        }
        $spcontent = Celtis_lib::htmltagsplit_keyword($spcontent[0], '<link', '/>',  array('stylesheet', 'href='), array());
        if(isset($spcontent[1])){
            $max = count($spcontent);
            for($n=1; $n <$max; $n++ ){
                if(false === in_array($spcontent[$n], $cssfile['outfile'])){
                    $cssfile['outfile'][] = $spcontent[$n];
                }
            }
        }
        $spcontent = Celtis_lib::htmltagsplit($spcontent[0], '<style', '/style>' );
        if(isset($spcontent[1])){
            $max = count($spcontent);
            for($n=1; $n <$max; $n++ ){
                $cssfile['inline'][] = $spcontent[$n];
            }
        }

        //CSS, JS 以外のタグ等を先に書き出す
        $mincontents = preg_replace('#^[ \t]*[\r\n]+#m', '', $spcontent[0]);

        if((int)$options['minify'] > 1){
            //CSS,JS ファイル統合毎のサブキャッシュ
            //テーマ、プラグインファイルの css, js に差異が生じたか
            $keyid = md5( $jsfile['key'] . $cssfile['key'] );
            $value = get_transient( $keyid );
            if (false !== $value) {
                if(!empty($value['jsfile'])){
                    if ( ! file_exists( $value['jsfile'] )) {     //ファイル存在するか?
                        delete_transient( $keyid );
                        $value = false;
                    }
                }
                if(!empty($value['cssfile'])){
                    if ( ! file_exists( $value['cssfile'] )) {     //ファイル存在するか?
                        delete_transient( $keyid );
                        $value = false;
                    }
                }
            }
            if (false === $value){
                //キャッシュディレクトリ作成
                $upload = wp_upload_dir();
                wp_mkdir_p( $upload['basedir'] . "/celtispack/css" ); 
                wp_mkdir_p( $upload['basedir'] . "/celtispack/js" ); 
            }
        }
        else
            $value = false;

        //css 外部、コアファイル jsより上に移動するのみ
        foreach ($cssfile['outfile'] as $css)
            $mincontents .= $css  . PHP_EOL; 
        //css テーマとプラグインのファイル
        if (false !== $value){
            if (!empty($value['css']))
                $mincontents .= $value['css'] . PHP_EOL; 
        }
        else {
            if((int)$options['minify'] == 4)
                $cssmin = new CSSmin();
            $outfile = '';
            foreach ($cssfile['infile'] as $css) {
                if(!empty($css)){
                    $match = array();
                    if((int)$options['minify'] > 1 && preg_match('#<link.+?href=[\'"]?(.+?\.css).+?\/>#i', $css, $match) ){
                        $c = @file_get_contents( $match[1]);
                        $d = pathinfo( $match[1]); 
                        //プラグインのディレクトリを指定して相対パスを絶対パスへ書き換える(既に縮小化してあっても必要)
                        $c = Celtispack_UriRewriter::wp_rewrite( $c, $d['dirname'], site_url() );
                        if((int)$options['minify'] == 4 &&  false === stripos( $match[1], '.min'))
                            $outfile .= trim($cssmin->run($c)) . PHP_EOL;
                        else
                            $outfile .= $c . PHP_EOL;
                    }
                    else
                        $mincontents .= $css . PHP_EOL;            
                }
            }
            if(!empty($outfile)){
                $fname  = '/celtispack/css/' . "{$keyid}_{$type}.css";
                if(file_put_contents( $upload['basedir'] . $fname, $outfile) !== false){
                    $setvalue['cssfile'] = $upload['basedir'] . $fname;
                    $setvalue['css'] = "<link rel='stylesheet' type='text/css' media='all' href='". $upload['baseurl'] . $fname . "' />"; 
                }
                $mincontents .= $setvalue['css'] . PHP_EOL;
            }
        }
        //css インラインはそのまま
        foreach ($cssfile['inline'] as $css){
            $mincontents .= $css . PHP_EOL;
        }

        //js 外部、コアファイル CSSより下に移動するのみ
        foreach ($jsfile['outfile'] as $js)
            $mincontents .= $js . PHP_EOL;
        //js テーマとプラグインのファイル
        if (false !== $value){
            if (!empty($value['js']))
                $mincontents .= $value['js'] . PHP_EOL; 
        }
        else {
            $outfile = '';
            foreach ($jsfile['infile'] as $js){
                if(!empty($js)){
                    $match = array();
                    if((int)$options['minify'] > 1 && preg_match('#<script[^>]*?src=[\'"]?(.+?\.js).+?<\/script>#i', $js, $match)){
                        if((int)$options['minify'] == 4 &&  false === stripos( $match[1], '.min'))
                            $outfile .= JSMin::minify(@file_get_contents( $match[1] )) . PHP_EOL;
                        else
                            $outfile .= @file_get_contents( $match[1] ) . PHP_EOL;
                    }
                    else
                        $mincontents .= $js . PHP_EOL;            
                }
            }
            if(!empty($outfile)){
                $fname  = '/celtispack/js/' . "{$keyid}_{$type}.js";
                if(file_put_contents( $upload['basedir'] . $fname, $outfile) !== false){
                    $setvalue['jsfile'] = $upload['basedir'] . $fname;
                    $setvalue['js'] .= "<script type='text/javascript' src='". $upload['baseurl'] . $fname . "'></script>";
                }
                $mincontents .= $setvalue['js'] . PHP_EOL;
            }
        }
        //js インライン(ポストID等の固有情報を含む場合があるのでサブキャッシュからは除外)
        foreach ($jsfile['inline'] as $js){
            $mincontents .= $js . PHP_EOL;
        }
        if(!empty($setvalue)) {
            if(!empty($setvalue['cssfile']))
                $value['cssfile'] = $setvalue['cssfile'];
            if(!empty($setvalue['jsfile']))
                $value['jsfile'] = $setvalue['jsfile'];
            set_transient( $keyid, $setvalue, DAY_IN_SECONDS);
        }

        if($pgid !== false){
            $cache['css_js_minify']['content'] = $mincontents;
            if(!empty($value['cssfile']))
                $cache['css_js_minify']['cssfile'] = $value['cssfile'];
            if(!empty($value['jsfile']))
                $cache['css_js_minify']['jsfile'] = $value['jsfile'];
            $cache['css_js_minify']['md5'] = md5($contents . $options['minify']);
            set_transient( $pgid, $cache, DAY_IN_SECONDS );
        }
        return $mincontents;
    }

少し詳しく見ていきます

並べ替え

まずは入力データをJS,CSS、それ以外のコードに分類していきます

さらに、JS,CSSは、テーマとプラグインのファイル、外部サイトのファイル(WordPressのコアを含む)、インラインのファイルに分類します

ファイルの重複がないかチェックして、並べ替えを行います

並べ替えだけで出力ならこれだけです

ファイル統合

統合の対象は、テーマとプラグインのファイルだけです

外部ファイルは、統合の対象外としていますし、インラインのコードにはポストID等のページ特有の情報が含まれることが多いので統合せずにそのまま出力するのが吉です (^^)

JSファイルの統合は、対象となるJSファイルを読み込んで一つのファイルに書き出せばOKです

CSSファイルの場合は、その中で @import や url で相対アドレスでファイルを指定されていたらその部分を絶対アドレスに書き換える必要があります。さらに画像URLを base64 に置き換えることも行います

また、子テーマの style.css の場合には、先頭で親テーマの style.cssを @import している場合がありますが、この場合は、絶対アドレスへの書き換えでなく親のデータを読み込んで置き換える必要があります

この部分の処理を wp_rewrite という関数を作成して行っています

    public static function wp_rewrite($css, $currentDir, $site_url) 
    {
        self::$_currentDir = parent::_realpath($currentDir);
        self::$_docRoot = parent::_realpath($site_url);
        
        preg_replace('/
            url\\(      # url(
            \\s*
            ([^\\)]+?)  # 1 = URI (assuming does not contain ")")
            \\s*
            \\)         # )
            /x', 'url($1)', $css);  //_trimUrls       

        $css = preg_replace_callback('/@import\\s+url\\(\\s*([\'"])(.*?)[\'"]\\s*\\)/',array('Celtispack_UriRewriter', '_processUriCB'), $css);
        $css = preg_replace_callback('/@import\\s+([\'"])(.*?)[\'"]/',array('Celtispack_UriRewriter', '_processUriCB'), $css);
        $css = preg_replace_callback('/url\\(\\s*([^\\)\\s]+)\\s*\\)/',array('Celtispack_UriRewriter', '_processUriCB'), $css);
        return $css;
    }

    //Minify_CSS_UriRewriter クラスの _processUriCB() protected 宣言されていなので絶対パス用に修正して定義
    private static function _processUriCB($m)
    {
        // $m matched either '/@import\\s+([\'"])(.*?)[\'"]/' or '/url\\(\\s*([^\\)\\s]+)\\s*\\)/'
        $isImport = ($m[0][0] === '@');
        // determine URI and the quote character (if any)
        if ($isImport) {
            $quoteChar = $m[1];
            $uri = $m[2];
        } else {
            // $m[1] is either quoted or not
            $quoteChar = ($m[1][0] === "'" || $m[1][0] === '"')
                ? $m[1][0]
                : '';
            $uri = ($quoteChar === '')
                ? $m[1]
                : substr($m[1], 1, strlen($m[1]) - 2);
        }
        // analyze URI
        if ('/' !== $uri[0]                  // root-relative
            && false === strpos($uri, '//')  // protocol (non-data)
            && 0 !== strpos($uri, 'data:') ) // data protocol
        {  
            $uri = parent::rewriteRelative($uri, self::$_currentDir, self::$_docRoot);
            if (false === strpos($uri, 'data:')){
                $uri = self::$_docRoot . $uri;
                $pcss = '';
                if((int)self::$_option >= 2){
                    //親の @import style.css ファイルなら読み込む
                    $theme = basename( get_template_directory_uri() );
                    $match = array();
                    $matchstr = "#($theme\/style\.css)#i";
                    if (preg_match($matchstr, $uri, $match)){
                        $pcss = @file_get_contents( $uri);
                        $d = pathinfo( $uri); 
                        $pcss = Celtispack_UriRewriter::wp_rewrite($pcss, $d['dirname'], site_url());
                        return $pcss . PHP_EOL;
                    }
                }
                if((int)self::$_option >= 3){
                    //Data uri の画像を base64 変換
                    $match = array();
                    if (preg_match("#(\.jpe?g|\.png|\.gif|\.ico)#i", $uri, $match)){
                        $type = strtolower(substr($match[0], 1));
                        if ($type == 'jpg')
                            $type = 'jpeg';
                        elseif ($type == 'ico')
                            $type = 'x-icon';

                        $base = base64_encode( @file_get_contents( $uri) );
                        $leng = empty($base)? 0 : strlen($base);
                        if($leng > 0 && $leng < 4096){
                            $quoteChar = '';
                            $uri = "data:image/{$type};base64,{$base}";
                        }
                    }
                }
            }
        }
        return $isImport
            ? "@import {$quoteChar}{$uri}{$quoteChar}"
            : "url({$quoteChar}{$uri}{$quoteChar})";
    }

ここが統合処理プログラムの肝となっています

テーマやプラグインのディレクトリと WordPress の site_url を元に相対アドレスを絶対アドレス URL へ書き換えます

 縮小化

JSの縮小化は、JSMin::minify() を呼び出すだけです

CSSの縮小化は、一手間増えますが

$cssmin = new CSSmin(); でインスタンスを作成して $cssmin->run() で行います

縮小化の中の処理は、コメントや空白を取り除いたり、コードを最適化したりを正規表現の処理を駆使して行っており、ほとんどブラックボックスという感じですが、JSやCSSファイルがエラー等のない問題ないファイルであるという保証がないので正常に処理されるかどうかはわかりません

結構リスクは高いということです

WordPress Celtis-one テーマのパフォーマンスを測定してみる で効果を検証してみましたが、リスクをおかしてまで縮小化する必要はないように感じました (^^)

キャッシュ

統合、縮小化したファイルはキャッシュとしてファイルに書き出します

ポイントは、対象となるデータから md5 を用いて一意となる値を計算して識別するところです

この値はプラグインの追加や削除、ギャラリーやコメント欄の使用により変化してくるので、10個程度のパターンがあるかも知れないと考えています

ポストIDデータが含まれるとページ毎にキャッシュファイルが生成されるので注意です

作成したキャッシュファイルは、transient api を使って管理します

だいたいこんな感じででしょうか

プログラム作成は結構悩みながら行ったのですが、効果はわずかだったので少し残念でしたが勉強にはなりました (^^)

ソースコードは、ダウンロードした modules/css_js_minify/css_js_minify.php ファイルを参照して下さい


まとめ記事紹介

go-to-top