Незабвенный герой Анатолия Папанова говаривал, что ежели человек идиот, то это надолго. Мне сейчас кажется, что я как раз в такой ситуации :-). Это я о том, как в джанговских шаблонах делать ссылки на JS, CSS и прочую media.

Я всегда пропагандировал простой способ: прокидывать в шаблоны переменную MEDIA_URL, используя стандартный контекст-процессор media. А в шаблоне просто составлять слова рядом:

<link rel="stylesheet" href="{{ MEDIA_URL }}css/style.css">

Это типа работает, но у такого способа есть пара минусов:

И тут вдруг у меня сошлись в голове какие-то правильные мысли с разных сторон, и я написал себе удобный тег для формирования ссылок на media-файлы в шаблонах с парой полезных дополнений.

Флажки "timestamp", "no-timestamp" и "absolute" можно использовать одноврменно (через запятую).

Выкладывать отдельным проектом я его не хочу, потому что уж очень маленький. Вместо этого я хочу собрать мнения по поводу полезности самого подхода и может быть предложить включить его в Django. А пока вот его небольшой код:

def _absolute_url(url):
    if url.startswith('http://') or url.startswith('https://'):
        return url
    domain = Site.objects.get_current().domain
    return 'http://%s%s' % (domain, url)

@register.simple_tag
def media(filename, flags=''):
    flags = set(f.strip() for f in flags.split(','))
    url = urlparse.urljoin(settings.MEDIA_URL, filename)
    if 'absolute' in flags:
        url = _absolute_url(url)
    if (filename.endswith('.css') or filename.endswith('.js')) and 'no-timestamp' not in flags or \
       'timestamp' in flags:
        fullname = os.path.join(settings.MEDIA_ROOT, filename)
        if os.path.exists(fullname):
            url += '?%d' % os.path.getmtime(fullname)
    return url

Комментарии: 36 (особо ценных: 1)

  1. Андрей

    Особо ценный комментарий

    Мне кажется, подход хороший.

    С использованием MEDIA_URL еще одна проблема: довольно часто нужно, чтобы стиль 404.html и 500.html соответствовал общему стилю сайта. Поэтому приходится наследовать эти шаблоны от base.html, но, фишка в том, что для 500 и 404 в Django RequestContext не используется, поэтому от использования MEDIA_URL в base.html приходится отказываться.

    С тэгом проще, потому что {% load media %} можно добавить в любой шаблон, в том числе и в 404 и 500.

  2. Malcolm Tredinnick

    (I can read the post, but I'm not up to writing my comment in Russian.)

    The "style.css?..." style of versioning has one problem: it isn't always cache-friendly. Some caches, and I believe Squid is the main example used here, won't cache things with query strings in the URL, on the grounds that they may change. So the effect of version using query strings results in no-caching. Incorporating the version into the resource name is recommended there.

    Secondly, your _absolute_url() method always uses "http://...". On a secure (https-based) site, that will lead to warnings on some browsers, warning about retrieving both secure and insecure content on the same page. You really need access ot the request there so that you have access to request.is_secure().

    Both of these are still possible in a tag, although the second one requires using RequestContext, again, since you need to retrieve "request" from the template's context (if it exists, otherwise default to "http://...".

  3. Dima Kuchin

    Забавно, я буквально несколько недель назад реализовал похожую логику: {% media js "file" %} и {% media css "file" %}.
    Только версионность обеспечивалась через вид filename_vVERSION.js, а в mod_rewrite _vVERSION убиралось.

  4. Google user

    Альтернативный подход к компилляции медиа-ресурсов:
    http://code.google.com/p/app-engine-patch/wiki/MediaGenerator

  5. Zigzag

    Иван, а есть ли способ решения еще одной проблемы с {{ MEDIA }}?

    Суть ее в следующем. Есть, например, тестовый сервер, на котором доступ к сайту может осуществляться, например по внешнему IP по следующей схеме xxx.xxx.xxx.xxx/mysitecom. На продакшене, естественно доступ должен быть по домену mysite.com. Проблема в том, что при переходе по относительным урлам сайт перенаправляет на, например, xxx.xxx.xxx.xxx/mysitecom/news, а на xxx.xxx.xxx.xxx/news.

    Возможно ли сделать какой-то переключатель переменной {{ MEDIA }}что ли, чтобы не приходилось на тестовом сервере вбивать в ручную адрес сайта постоянно, но при этом на продакшене, все осталось работать, как работает?

  6. Александр Соловьёв

    я написал себе удобный тег для формирования ссылок на media-файлы

    Ахахах. :D Вот решение из Byteflow, немного иначе сделанное (логично, приоритеты стояли иначе), написанное с года полтора назад. ;)

    Incorporating the version into the resource name is recommended there.

    I'm not sure that Squid should be considered there, as purpose of cache there is to decrease load time for user, not to decrease load on server (loading of static files is not that heavy task). But incorporation of the version into the resource name will require setup of mod_rewrite, more logic in code and so on. So I personally prefer going this way too.

  7. Pavel Reznikov

    Zigzag, можно задавать эти вещи, например, переменными окружения, а в settings.py читать из них.

    Второе, согласен с Malcolm Tredinnick по поводу кеширования. Как по мне, то пусть этим занимается http и сервер, который отдает статику. Ну либо сделать эту штуку по-умолчанию выключенной.

    А тэг хороший. Может добавить его в джанго снипеты?

  8. Иван Сагалаев

    Malcolm Tredinnick:

    although the second one requires using RequestContext, again, since you need to retrieve "request" from the template's context

    Yeah, that's why I didn't use it. There are generally two ways for "absolutizing" URLs: either rely on the request taking both domain and scheme from it or rely on the Site model. I prefer Site because it doesn't bind you to http requests (think standalone scripts). But I really think we might add a 'scheme' field to the Site model. I think it even would be backwards compatible.

    Alexander Solovyov:

    I'm not sure that Squid should be considered there, as purpose of cache there is decrease load time for user, not to decrease load on server (loading of static files is not that heavy task). But incorporation of the version into the resource name will require setup of mod_rewrite, more logic in code and so on. So I personally prefer going this way too.

    If I understand Malcolm's point Squid would strip "Expires" from such responses so browsers won't cache it too (though I didn't test, may be it's fixed). But I agree that setting up a rewrite is awkward...

  9. Иван Сагалаев

    Возможно ли сделать какой-то переключатель переменной {{ MEDIA }}что ли

    Вполне. Обычно settings для разработки и продакшна разные. Вот первый нагуглившийся рецепт: http://martinjansen.com/2008/10/20/django-settings-files-for-development-and-production/

  10. http://maxidoors.ru/

    Malcolm Tredinnick:
    You are almost quite right. In fact, if there is a not-null QUERY_STRING (something after ?), then browser will ALWAYS sent GET If-Not-Modified, even if you tell that this file will be unexpired till year 2050.

    That is why is strongly required to setup such rewriting, when urls are rewrited from:

    [http://yandex.ru/logotype.png?12345678](http://yandex.ru/logotype.png?12345678)
    

    to something like

    [http://yandex.ru/logotype.12345678.png](http://yandex.ru/logotype.12345678.png)
    

    and not by timestamp, but by CRC32(contents), because time, when file will go to different servers is unpredictable.

  11. Иван Сагалаев

    In fact, if there is a not-null QUERY_STRING (something after ?), then browser will ALWAYS sent GET If-Not-Modified, even if you tell that this file will be unexpired till year 2050.

    Max, those are unrelated. If-Not-Modified is sent whenever a response before it has Last-Modified header. But we're talking about browsers not making a request at all when a response before it has Expires header.

    Most servers server static files with both Expires and Last-Modified set even when requested with a query string. So time-based caching actually does work in browsers (given that no intermediate proxies will eat the header).

    because time, when file will go to different servers is unpredictable

    This depends on your deployment software. A good system should retain last modified time (dpkg does for example). Counting CRC might be a bit expensive because it will be done on every page request that generates links to media files.

  12. Иван Сагалаев

    Most servers server static files with both Expires and Last-Modified

    Uhm... Just checked. Lighttpd doesn't set Expires actually. This is then cached by some internal browser politics about expire times (Firebug shows about 1 hour time). Which is BTW fully compliant with HTTP and makes Squid behavior irrelevant to browsers. Which is good!

  13. Макс

    Идея хорошая, мне понравилась, позаимствую для своего проекта.

  14. [...] Нашел тут вот довольно простой и удобный способ, как можно упростить работу со ссылками на статические файлы в Djan... [...]

  15. Александр Соловьёв

    Most servers server static files with both Expires and Last-Modified set

    Uh, really. I had to add for Nginx

    add_header last-modified "";
    

    to my locations with static files.

  16. Alexander Artemenko

    Мммм, для включения CSS и JS файлов уже есть reusable app — http://github.com/pelme/django-compress/. Использую его и очень доволен.

  17. Руслан Кеба

    Хорошее решение. спасибо.

    return 'http://%s%s' % (domain, url)

    а если возвращать нужно https?

  18. Сергей Шепелев

    Я понимаю смысл генерации урлов к медиа, хотя у нас с успехом используется тупой хардкод /s/style.css. Если надо поменять, все шаблоны в одном месте, греп поможет.

    Но я совершенно не понимаю заморочек с кешированием. Это, что, настолько часто меняется js и css? Тогда почему бы не поставить короткий expires? Статику отдавать всё равно достаточно дешево, плюс есть 304 и браузеры умеют.

  19. Иван Сагалаев

    Это, что, настолько часто меняется js и css?

    Не часто. Но это ситуация, когда одного раз достаточно :-). Достаточно выкатить небольшую фичку, которая требует наличия в CSS'е "display:none", и она начинает выглядеть очень криво у большинства людей. Как показывает практика, очень немногие при этом знают, что надо нажать Refresh, они просто видят сломанный сайт. Timestamp эффективно и просто решает проблему.

  20. Иван Сагалаев

    а если возвращать нужно https

    Там выше в комментариях есть про это. Пока красиво это не решается.

  21. Vitaliy

    Больше того, для css- и js-файлов это включено автоматически.

    еще было-бы клева иметь возможность отключить эту автоматику (например в settings)

  22. Иван Сагалаев

    Для этого есть флажок "no-timestamp".

  23. u960

    а если возвращать нужно https

    Там выше в комментариях есть про это. Пока красиво это не решается.

    то есть это нужно сделать либо return 'http://', либо return 'https://'? или все намного сложнее?

  24. [...] {% media %} а было бы неплохо выучит Python [...]

  25. Дмитрий Курилов

    Вопрос

    А зачем вообще нужна асолютизация или какие-то другие преобразования MEDIA_URL? Почему не оставить его как есть?

    MEDIA_URL = 'https://static.example.com/'
    # or
    MEDIA_URL = 'http://static.example.com/'
    # or just
    MEDIA_URL = '/media/'
    

    Предложение

    Расширить тег на предмет custom'ного вывода timestamp'а файла. Например, так:

    {% media "images/edit.png" %} -->
        {{ MEDIA_URL }}images/edit.png?1234567890
    {% media "images/edit.png{?%s}" %} -->
        {{ MEDIA_URL }}images/edit.png?1234567890
    {% media "images/edit{_v%s}.png" %} -->
        {{ MEDIA_URL }}images/edit_v1234567890.png
    {% media "images/edit{{_v%s}.png" %} -->
        {{ MEDIA_URL }}images/edit{_v%s.png?1234567890
    
  26. [...] статью про тег {% media %}. Идея очень понравилась, но не понял, для [...]

  27. ramusus.livejournal.com

    Подскажите плз, как исправить проблему: я положил код с тагом media в файл project/templatetags/media.py

    добавил в начало

    from django import template
    register = template.Library()
    

    при рендеринге шаблона отобразилась ошибка

    Caught an exception while rendering: global name 'urlparse' is not defined
    

    после подключения

    from urlparse import urljoin
    

    вывелась

    Caught an exception while rendering: global name 'settings' is not defined
    

    Почему в кастомных тагах могут быть не видны эти модули?

  28. redbaron

    Хм. Не отменяя достоинств тега {% media %} хочу сказать, что у меня в 404.html ссылки вида "{{ MEDIA_URL }}css/base.css" работают на ура.

  29. [...] к теме топика могу порекомендовать свой тег {% media %}. Он как раз появился из нежелания везде следить за [...]

  30. Alex Tracer

    а если возвращать нужно https

    Там выше в комментариях есть про это. Пока красиво это не решается.

    Решается. Достаточно заменить

    return 'http://%s%s' % (domain, url)
    

    на

    return '//%s%s' % (domain, url)
    

    Хоть урлы с двумя слешами вначале и выглядят странно, но работает всё отлично: на http:// страницах будет подразумеваться http://, а https:// - https://

  31. Ivan Sagalaev

    Решается. Достаточно заменить

    Недостаточно, к сожалению. Схемоотносительные URL'ы будут работать только на страницах того же сайта, на котором формируются. То есть это случай, когда я генерирую страницу сайта и хочу составить URL на картинку, лежащую здесь же. И тут, на самом деле и хоста даже не нужно, достаточно просто /media/path/filename.jpg.

    А абсолютные URL'ы нужны, когда вы генерируете что-то, что будет показываться или на другом сайте или в отосланном email'е. И вот там нужен целиковый [http://..](http://..)., потому что протокол у другого сайта может быть не таким, как у вашего, а в email его вообще нет.

  32. [...] в итоге именно для этого написал тег {% media %}, чтобы не таскать за собой контекст процессоры только [...]

  33. http://softwaremaniacs.org/blog/2009/03/22/media-tag/Как раз то что мне надо.Обьясните пожалуйста как его установить.Создал в папке с приложением папку templatetags закинул туда init.py,contex.py.Закинул в contex код, вначале написал:from django import templateimport urlparse,os,settingsregister = template.Library()Но таг не работает.

  34. Добрый вечер.Очень понравился топик по теме http://softwaremaniacs.org/blog/2009/03/22/media-tag/. Хотелось бы узнать, можно ли вместо timestamp'a как альтернативу использовать svn info о версии из рабочей копии на прод. сервере, и какие у этого подхода есть + и -. Спасибо.

  35. oleg.vodopyan

    полезный материал.. буду использовать)

    типа вопрос возник только по последних комментариях по поводу return 'http://%s%s' % (domain, url)

    А абсолютные URL'ы нужны, когда вы генерируете что-то, что будет показываться >или на другом сайте или в отосланном email'е. И вот там нужен целиковый http://..., потому что протокол у другого сайта может быть не >таким, как у вашего, а в email его вообще нет.

    если я правильно понял то ситуация с медиа на другом сайте определяется кодом

    if url.startswith('http://') or url.startswith('https://'):
        return url
    

    а строки

    domain = Site.objects.get_current().domain
    return 'http://%s%s' % (domain, url)
    

    это

    случай, когда я генерирую страницу сайта и хочу составить URL на картинку, лежащую здесь же. И тут, на самом деле и хоста даже не нужно, достаточно просто /media/path/filename.jpg.

    то есть можно return 'http://%s%s' % (domain, url) заменить на return '//%s%s' % (domain, url)?..

Добавить комментарий