Laravelでjsとcssのキャッシュ対策の方法を調べていて、 一応完成したので公開します。

結局、グローバルミドルウェアを使いました。 Controllerの処理が終わった後のレスポンスの内容(html出力)に対し、 正規表現を書けることでjsとcssのキャッシュ対策を行ってます。 キャッシュ対策はよくある、「?date={更新日}」を追記する方法です。

resources/views配下のbladeファイルを修正することなく、 このように書かれているjs, cssの定義部分が、

<link rel="stylesheet" media="all" href="/static/css/style1.css">
<link rel="stylesheet" href="/static/css/style2.css" media="all">

<script src="/static/js/common1.js"></script>
<script type="javascript" src="/static/js/common2.js"></script>
<script type="javascript" src="/static/js/common3.js" />
<script src="/static/js/common4.js" type="javascript" />

↓こうなります。

<link rel="stylesheet" media="all" href="/static/css/style1.css?date=20171204">
<link rel="stylesheet" href="/static/css/style2.css?date=20171204" media="all">

<script src="/static/js/common1.js?date=20171204"></script>
<script type="javascript" src="/static/js/common2.js?date=20171204"></script>
<script type="javascript" src="/static/js/common3.js?date=20171204" />
<script src="/static/js/common4.js?date=20171204" type="javascript" />

キャッシュ対策の対象となるのは * public/static/js * public/static/css 配下のそれぞれのjs, cssファイルです。 画面は、作成したのがグローバルミドルウェアなので全ページが対象になります。

viewを書くときはキャッシュ対策を意識せず、普通に記述すればいい(はず)です。 もし上手くいかない場合は、正規表現などを見直してみてください。

ただ、この方法だとキャッシュ対策しなくても良いものにも行ってしまうこともあり、 ページが全体的に遅くなりそうなので、 やるならグループミドルウェアにして対象を制限した方が良さげです。

色々と考えることがありますが、とりあえず以下にコードなどをまとめます。

middlewareを作成

$ php artisan make:middleware GlobalMiddleware

Kernel.phpに追記

$ vi app/Http/Kernel.php

以下の行を追記する。

  protected $middleware = [
      ~~省略~~
      \App\Http\Middleware\GlobalMiddleware::class,
      ~~省略~~
  ];

middlewareを編集

$ vi app/Http/Middleware/GlobalMiddleware.php

globalMiddleware.phpの中身は以下のようになりました。

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Support\Facades\Log;

class GlobalMiddleware
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {  
        // viewの処理が終わった後のレスポンスを取得
        $response = $next($request);
        $content = $response->content();

        // キャッシュ対策を行う。
        $response->setContent($this->execCacheMeasures($content, "{date}"));
        return $response;
    }

    /**
     * キャッシュ対策を行う
     * @param string $content - 置き換え対象のHTML
     * @param string $switch - キャッシュ対策の方法を指定する
     *                         {date}   = 末尾に日付を追記する対策
     *                         それ以外 = 固定文字列を追記する対策
     * @return string 置換後のHTML
     */
    private function execCacheMeasures($content, $switch = "") {
        try {
            // キャッシュ対策の方法を選択。
            $add_string = $switch == "{date}" ? "{date}" : $this->getCacheString();

            // 実施
            $content = $this->execCssCacheMeasures($content, $add_string);
            $content = $this->execJsCacheMeasures($content, $add_string);
            return $content;

        } catch (\Exception $e) {
            Log::error($e->getMessage());
        }
        return $content;
    }

    /**
     * キャッシュ対策用の、末尾に追記する固定文字列を取得する。
     * @return string 末尾に追記する固定文字列
     */
    private function getCacheString() {
        // 強制的にクリアさせる場合とか、固定の文字を入れたい場合。
        // リリースした日を取得できる場合はその日付とか。
        $add_string = "?date=" . date('Ymd');
        return $add_string;
    }

    /**
     * ファイルの最終更新日時を取得する(YYYYMMDD)。
     * @param string $file_path - ファイルへのパス
     * @return string 最終更新日。取得できなかった場合は空文字。
     */
    private function getUpdateDate($file_path) {
        if (\File::exists($file_path)) {
            $last_modified = \File::lastModified($file_path);
            if ($last_modified === false) return "";
            return date("Ymd", $last_modified);
        }
        return "";
    }

    /**
     * cssのキャッシュ対策を行う。
     * 正規表現の条件にマッチするlinkタグに対し、引数の文字列を追記する。
     * 対象: 「 href="/static/css/」が含まれるlinkタグのcssファイル。
     * 内容: .cssの後ろに、${add_string} を追記する。
     * @param string $content - 置き換え対象のHTML
     * @param string $add_string - 追記する文字列。
     *                             {date}の場合は各ファイルの最終更新日を追記。
     * @return string 置換後のHTML
     */
    private function execCssCacheMeasures($content, $add_string = "") {
        // 末尾に最終更新日を追加する場合
        if (!is_null($add_string) && $add_string == "{date}") {
            // 置換え対象のパスを取得
            preg_match_all('/<link.*href="(\/static\/.*\.css)"/i', $content, $matches);

            // 取得件数分ループ
            if (is_array($matches) && is_array($matches[1])) {
                foreach ($matches[1] as $match_line) {
                    // 該当ファイルの最終更新日を取得
                    $last_modified = $this->getUpdateDate(base_path() . "/public" .  $match_line);

                    // 取得できなかった場合は何もせず次の処理へ
                    if ($last_modified === "") continue;

                    // 最終更新日を追加する処理を実施
                    $pattern = '/<link(.*) href="' . str_replace('/', '\/', $match_line) . '"/i';
                    $replace = '<link$1 href="' . $match_line . '?date=' . $last_modified . '"';
                    $content = preg_replace($pattern, $replace, $content);
                }
            }
        // 固定の文字を追加する場合
        } else if (!is_null($add_string) && $add_string != "" && is_string($add_string)) {
            $pattern = '/<link(.*) href="\/static\/css\/(.*)\.css"/i';
            $replace = '<link$1 href="/static/css/$2.css' . $add_string . '"';
            $content = preg_replace($pattern, $replace, $content);
        }
        return $content;
    }

    /**
     * jsのキャッシュ対策を行う。
     * 正規表現の条件にマッチするscriptタグに対し、引数の文字列を追記する。
     * 対象: 「<script src="/static/js/」が含まれるjsファイル。
     * 内容: .jsの後ろに、${add_string} を追記する。
     * @param string $content - 置き換え対象のHTML
     * @param string $add_string - 追記する文字列。
     * @return string 置換後のHTML
     */
    private function execJsCacheMeasures($content, $add_string = "") {
        // 末尾に最終更新日を追加する場合
        if (!is_null($add_string) && $add_string == "{date}") {
            // 置換え対象のパスを取得
            preg_match_all('/<script.* src="(\/static\/js\/.*\.js)"/i', $content, $matches);

            // 取得件数分ループ
            if (is_array($matches) && is_array($matches[1])) {
                foreach ($matches[1] as $match_line) {
                    // 該当ファイルの最終更新日を取得
                    $last_modified = $this->getUpdateDate(base_path() . "/public" .  $match_line);

                    // 取得できなかった場合は何もせず次の処理へ
                    if ($last_modified === "") continue;

                    // 最終更新日を追加する処理を実施
                    $pattern = '/<script(.*) src="' . str_replace('/', '\/', $match_line) . '"/i';
                    $replace = '<script$1 src="' . $match_line . '?date=' . $last_modified . '"';
                    $content = preg_replace($pattern, $replace, $content);
                }
            }
        // 固定の文字を追加する場合
        } else if (!is_null($add_string) && $add_string != "" && is_string($add_string)) {
            $pattern = '/<script(.*) src="\/static\/js\/(.*)\.js"/i';
            $replace = '<script$1 src="/static/js/$2.js' . $add_string . '"';
            $content = preg_replace($pattern, $replace, $content);
        }
        return $content;
    }
}

補足

コードの24行目、

    $response->setContent($this->execCacheMeasures($content, "{date}"));

execCacheMeasures()の第二引数で、日付の取得方法を指定できます。 * {date} = 対象ファイルの更新日 * それ以外 = 今日の日付

更新日指定で、ファイルが存在しない場合は何もしない仕様になっております。

こんなのでよかったら使ってやってください。 もしバグとか見つけたらご連絡いただけると嬉しいです。

参考

こちらのサイトを参考にさせていただきました。ありがとうございます。