Django шаблоны и css: есть ли обходной путь для того, чтобы мои теги ссылок preload соответствовали моим относительным путям css к источникам шрифтов?
Фон
Я только что начал использовать Digital Oceans Spaces для хранения статических файлов для моего веб-приложения Django (подключенного с помощью библиотеки django-storages
). Раньше я использовал элементы link
в head
базового шаблона для предварительной загрузки шрифтов, чтобы избежать вспышек при переключении шрифтов во время загрузки страницы (как в случае с 'Mona Sans') или когда элемент, использующий пользовательский шрифт, показывается в первый раз (я использую шрифт 'Pixelated' в элементе dialog
).
Выпуск
Однако теперь есть несоответствие в url, создаваемых тегом шаблона static
в моих шаблонах Django, и url, создаваемых относительными путями в моем css-файле.
Шрифты загружаются нормально, используя относительный путь в css-файле, но им не хватает параметров запроса, поэтому предварительно загруженные ресурсы (с параметрами запроса) на самом деле не используются (что приводит к кратковременной вспышке замены шрифтов и предупреждениям в консоли).
Дополнительно, я не знаю, вызовет ли отсутствие параметров запроса проблемы, когда я реализую защиту от чтения с предварительно подписанными URL.
Шаблон Django
<link rel="preload" href="{% static 'fonts/Mona-Sans.woff2' %}" as="font" type="font/woff2" crossorigin/>
<link rel="preload" href="{% static 'fonts/Pixelated.woff2' %}" as="font" type="font/woff2" crossorigin/>
При таком использовании тега static получается URL следующего вида:
https://{region}.digitaloceanspaces.com/{bucket_name}/static/fonts/Pixelated.woff2?{bunch of query parameters}
css
@font-face {
font-family: 'Mona Sans';
src:
url('../fonts/Mona-Sans.woff2') format('woff2 supports variations'),
url('../fonts/Mona-Sans.woff2') format('woff2-variations');
font-weight: 200 900;
font-stretch: 75% 125%;
font-display: swap;
}
Этот относительный путь приводит к такому URL-адресу:
https://{region}.digitaloceanspaces.com/{bucket_name}/static/fonts/Pixelated.woff2
Пример предупреждения консоли браузера
Ресурс https://sfo3.digitaloceanspaces.com/spaces-bucket-name/static/fonts/Pixelated.woff2?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=blah%blah%blah%blah%blah%blah_request&X-Amz-Date=20240709T172841Z&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Signature=blahblahblahblahblah был предварительно загружен с помощью предварительной загрузки ссылок, но не использовался в течение нескольких секунд после события загрузки окна. Пожалуйста, убедитесь, что он имеет соответствующее
as
и что он загружен намеренно.
Потенциальные решения
Вот потенциальные решения, которые мне приходят в голову:
- Как-то обслуживать более мелкие/основные статические файлы, такие как шрифты и иконки svg, с того же сервера, который обслуживает проект Django (в Django возможно ли разделить определенные статические ресурсы на два разных способа обслуживания статических файлов), избегая DO CDN вообще .
- Придумайте способ вставлять полный URL ресурса в мои css файлы
- Хранить более мелкие/основные статические файлы, такие как шрифты и иконки svg, где-нибудь в публичном репозитории и просто использовать абсолютные URL на этот репозиторий как в css, так и в html head .
Есть ли лучшее решение, о котором я не подумал? Если нет, то какое из возможных решений обеспечит наилучшую производительность фронтенда моего приложения?
Множество бэкендов для хранения данных
Поскольку мой проект имеет дело с большим количеством аудио, которое я подаю как статические файлы, и поскольку все мои другие статические файлы (css, js, svg, fonts, jpg, mp4 и др. ) имеют очень маленькие размеры (от 497 байт до 667 кб), которые Digital Oceans Spaces рекомендует обслуживать другим способом, я решил просто обслуживать статические файлы с того же сервера, который обслуживает мой проект django, и я реализовал модель для аудиофайлов, которые будут обслуживаться с Digital Oceans Spaces.
Подводя итог, я все еще использую библиотеку django-storages
, но у меня есть один способ обслуживания статических файлов (стандартная стратегия staticfiles, которая требует выполнения команды collectstatic
каждый раз, когда статический файл изменяется или добавляется) и два разных способа обслуживания медиафайлов (медиафайлы моего сайта и медиафайлы пользователя). Ниже показан мой settings.py
и упрощенный вид моего models.py
, чтобы вы могли увидеть код, который я использовал для реализации этой стратегии статических/медиафайлов.
Пара заметок:
- использование пакета django-environ для переменных окружения
- 'default' - схема хранения (с
user_media_storage_backend
), которая используется для пользовательских медиафайлов - 'appmedia' - схема хранения (с
app_media_storage_backend
) для медиафайлов моего сайта- Мне нравится разделять эти две схемы просто потому, что я могу установить отдельные ведра пространства Digital Oceans для любого типа медиафайлов (и задать разные настройки перезаписи)
- переменная окружения
USE_SPACES
может быть установлена в значение False, чтобы использовать обычный бэкэнд'django.core.files.storage.FileSystemStorage'
для хранения всех медиафайлов (для среды разработки, при желании) - Вы увидите, что я использую вызываемые функции для установки путей и имен файлов для ImageField и FileField (см. документацию Django для FileField)
- Вы также увидите, что я префиксирую имена файлов случайной последовательностью букв ascii, чтобы при обращении к любому ресурсу не приходилось перебирать множество объектов, чтобы найти нужный объект в Spaces ( как рекомендуется в документации Digital Oceans Spaces)
settings.py
profile > models.py
Это поле ImageField в моей модели профиля пользователя будет использовать бэкэнд хранилища, который я определил как 'default'
:
from django.db import models
import random
from string import ascii_letters
# Returns five-char random string to be prepended to filename (so computer can find file faster as suggested here: https://docs.digitalocean.com/products/spaces/concepts/best-practices/)
def genRandomString():
randomPrefix = ''.join(random.choice(ascii_letters) for i in range(5))
return randomPrefix
# with how this function works, there would need to be validation on the forms that handle this file upload so that they only accept jpg and png files
def profile_pic_file_path(instance, filename):
randomString = genRandomString()
jpg_extensions = ('.jpg', '.JPG', '.jpeg', '.JPEG')
png_extensions = ('.png', '.PNG')
if filename.endswith(jpg_extensions):
ext = '.jpg'
elif filename.endswith(png_extensions):
ext = '.png'
# this is just a catch-all in case a non-jpg or -png file slips by validation
else:
ext = filename
return f'profile_pics/{randomString}_userslug_{instance.slug}_pic{ext}'
class UserProfile(models.Model):
# ... bunch of fields ...
profile_pic = models.ImageField(null=True, blank=True, upload_to=profile_pic_file_path)
dialogues > models.py
На что стоит обратить внимание:
- Мне нужно загрузить модуль
storages
, чтобы я мог выбрать нестандартный метод хранения файлов .
- здесь я не так беспокоюсь о проблемах с расширением файлов, поскольку загружать эти файлы буду я, а не пользователи
- моя функция для установки пути и имени файла может обращаться к связанным объектам, что очень удобно ( см. документацию Django для FileField).
from django.db import models
from django.core.files.storage import storages
import random
from string import ascii_letters
def select_storage():
return storages['appmedia']
# Returns two-char random string to be prepended to filename (so computer can find file faster as suggested here: https://docs.digitalocean.com/products/spaces/concepts/best-practices/)
def genRandomPrefix():
randomPrefix = ''.join(random.choice(ascii_letters) for i in range(2))
return randomPrefix
def dialogue_audio_file_path(instance, filename):
dialogue_id = instance.original_dialogue.dialogue_slug
randomPrefix = genRandomPrefix()
return f'dialogue_audios/{dialogue_id}/{randomPrefix}-{filename}'
class DialogueAudioFile(models.Model):
audio_file = models.FileField(
storage=select_storage, upload_to=dialogue_audio_file_path
)
original_dialogue = models.ForeignKey(
Dialogue, on_delete=models.CASCADE,
)