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,
    )
Вернуться на верх