はじめに
Djangoで本格的にwebアプリ開発をするようになると、CSSやJSなどの静的(static)ファイルを扱うことが増えてきますが、その時に悩みの種になるのが静的ファイルのキャッシュです。
ブラウザはページの通信・ロード時間を減らすために、しばしば静的ファイルをキャッシュに保存しておき、再アクセスした時に使い回します。そのため、開発者側が静的ファイルを書き換えても、ブラウザ側で最新のデータで読み込まれずに表示が崩れたり、予期しない挙動をする可能性があります。
cache bustingはhtml上の静的ファイルのパスに対しクエリ文字列を付加し、パスを変更することで、閲覧ユーザーに常に最新のファイルを読み込ませるテクニックとして知られています。
Djangoではsettingsに特定の定数を追加したり、 django-static-md5url
というライブラリを用いたりすることで、静的ファイルのパスにハッシュ文字列を付加し、毎回強制読み込みさせることができます。
ただ、毎回読み込ませるようにしてしまうと、ページのロード時間が無駄に長くなったり、キャッシュが無駄に蓄積されていくというユーザー側のデメリットが出てきます。
そこで今回は、ハッシュ文字列ではなくファイル自体の更新日時を付加することで、ファイルを更新した時だけ強制読み込みさせるようにします。
環境
項目 | 内容 |
---|---|
Python | 3.9.1 |
Django | 3.2.9 |
使用ブラウザ | Safari 15.1 |
実際のコード
前座: 環境構築
最初にプロジェクト・アプリを作成します。
pip install django
django-admin startproject sample_proj
cd sample_proj
django-admin startapp sample_app
フォルダ構造
追加と書いてあるファイルはデフォルトで存在しないファイルです。
不要と書いてあるファイルは今回は使いません。
sample_proj/
├── sample_app/
│ ├── static/
│ │ ├── css/
│ │ │ └── index.css: 追加
│ ├── templates/
│ │ └── index.html: 追加
│ ├── templatetags/
│ │ └── cache_busting.py: 追加
│ ├── migrations/
│ ├── admin.py: 不要
│ ├── apps.py
│ ├── models.py: 不要
│ ├── tests.py: 不要
│ ├── urls.py: 追加
│ └── views.py
├── sample_proj/
│ ├── asgi.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
└── manage.py
導入する前の各種ファイルの中身
まずは表示するページのデータを作成します。
{% load static %}
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<link href="{% static 'css/index.css' %}" rel="stylesheet" type="text/css">
<title>Cache busting test</title>
</head>
<body>
<p class="test-text">hogehogefugoooo</p>
</body>
</html>
.test-text {
color: #ff0000;
}
views.pyはhtmlをレンダリングするだけの簡単なコード。
from django.shortcuts import render
def index(request, **kwargs):
return render(request, 'index.html')
urls.pyは多くのDjango開発でそうするように、sample_projとsample_appそれぞれに作成しておきます。
from django.urls import path
from . import views
urlpatterns = [
path('', views.index, name='index'),
]
from django.contrib import admin
from django.urls import path, include # includeを追加
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('sample_app.urls')), # 追加
]
最後に、settings.pyを編集します。 INSTALLED_APPS
に 'sample_app'
を追加して認識できるようにします。
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'sample_app', # 追加
]
試しに起動する
今回はmodels.pyを定義していませんが、migrateしておきましょう。
collectstaticでエラーが出る場合は、大抵 STATIC_ROOT
の設定し忘れです。
python manage.py migrate
python manage.py collectstatic
python manage.py runserver
無事に起動されたら、ブラウザで http://127.0.0.1:8000 にアクセスしてみます。以下のような画面が表示されるはずです。
これで最低限の準備が整いました。
本題: cache bustingの実装
さて、ここからcache bustingのコードを実装していきます。今回はテンプレートエンジンとの親和性が高い、テンプレートタグ(simple_tag)を用いて実装します。
templatetagについて知らない方は、以下のサイトを見ると良いと思います。
先ほどのフォルダ構造で追加した static_cache.py
に書き込みます。
from pathlib import Path
from django import template
from django.conf import settings
from django.templatetags.static import static
register = template.Library()
@register.simple_tag
def static_cache(filepath) -> str:
"""静的ファイルのURLにファイルの更新日時クエリを付加する
"""
# Django標準機能を使ってhtml埋め込み用のパスを取得
res_path = static(filepath)
# mtimeを取得する
full_filepath = Path(getattr(settings, 'STATIC_ROOT', '')).joinpath(filepath)
file_mtime = str(int(full_filepath.stat().st_mtime))
# パスに結合
res_path += '?v=' + file_mtime
return res_path
解説
余談なので折りたたみました
- staticタグを経由して静的ファイルのパスを取得
- 実際のファイルの更新日時をpathlibライブラリ(python標準)で取得
- 更新日時を整数に切り捨て(=秒単位に変換)、クエリ文字列として追記
- 編集したパスを返す
厳密には、テンプレートタグの static
はコード中では do_static()
という関数になっていて、今回使ったものとは異なります。詳しく知りたい方は以下のソースコードをご覧ください。
https://github.com/django/django/blob/stable/3.2.x/django/templatetags/static.py
実装したテンプレートタグを実際に使ってみます。先ほど作成した index.html
を編集します。
{% load cache_busting %} <!--変更-->
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<link href="{% static_cache 'css/index.css' %}" rel="stylesheet" type="text/css"> <!--変更-->
<title>Cache busting test</title>
</head>
<body>
<p class="test-text">hogehogefugoooo</p>
</body>
</html>
編集したら、試しにアプリを再起動し、最初と同じページ内容が表示されることを確認してください。
開発者ツールを見ると、cssのパスに以下のように更新日時のクエリが追加されています。
<link href="/static/css/index.css?v=1639965343" rel="stylesheet" type="text/css">
動作確認
最後に、実装したcache bustingが正常に動作することを確認します。
index.cssを以下のように変更します。
.test-text {
color: #00ff00;
}
編集したらcollectstaticしておきます。
python manage.py collectstatic
ページを(通常のF5)リロードすると、ページに表示されていた文字列の色が変わりました。
開発者ツールでcssファイルのパスを確認すると、先ほどと違うクエリ文字列になっていることがわかります。
<!-- 先ほどのクエリ文字列は 1639965343 だった -->
<link href="/staticfiles/css/index.css?v=1639967407" rel="stylesheet" type="text/css">
最後に
cache bustingを用いることで、ユーザーが毎回キャッシュを削除してリロードしなくても、サーバー側が使わせたい静的ファイルを強制読み込みさせることが可能になります。
静的ファイルを利用する際には、ぜひ導入してみましょう!