Django 静态文件版本控制

发布于 2025-01-02 04:31:13 字数 675 浏览 1 评论 0原文

我正在研究一些针对静态文件及其更新问题的通用解决方案。

示例:假设有一个带有 /static/styles.css 文件的网站 - 并且该网站使用了很长时间 - 所以很多访问者在浏览器中缓存了这个文件

现在我们对此 css 文件进行更改,并在服务器上更新,但有些用户仍然有旧版本(尽管服务器返回修改日期)

明显的解决方案是向文件 /static/styles.css?v=1.1 添加一些版本,但在此案例开发人员必须跟踪此文件中的更改并手动增加版本

第二种解决方案是计算文件的 md5 哈希值并将其添加到 url /static/styels.css/?v={mdp5hashvalue} ,这看起来好多了,但 md5 应该以某种方式自动计算。

他们可能的方式我看到它 - 创建一些像这样的模板标签

{% static_file  "style.css" %}

,它将呈现

<link src="/static/style.css?v=md5hash">

但是,我不希望这个标签在每个页面加载时计算 md5,并且我不想将哈希存储在 django-cache 中,因为那样我们将有更新文件后清除...

有什么想法吗?

I'm working on some universal solution for problem with static files and updates in it.

Example: let's say there was site with /static/styles.css file - and site was used for a long time - so a lot of visitors cached this file in browser

Now we doing changes in this css file, and update on server, but some users still have old version (despite modification date returned by server)

The obvious solution is to add some version to file /static/styles.css?v=1.1 but in this case developer must track changes in this file and manually increase version

A second solution is to count the md5 hash of the file and add it to the url /static/styels.css/?v={mdp5hashvalue} which looks much better, but md5 should be calculated automatically somehow.

they possible way I see it - create some template tag like this

{% static_file  "style.css" %}

which will render

<link src="/static/style.css?v=md5hash">

BUT, I do not want this tag to calculate md5 on every page load, and I do not want to store hash in django-cache, because then we will have to clear after updating file...

any thoughts ?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(9

坏尐絯 2025-01-09 04:31:13

Django 1.4 现在包含 CachedStaticFilesStorage这正是您所需要的(嗯......几乎)。

自 Django 2.2 ManifestStaticFilesStorage<应使用 /code>代替 CachedStaticFilesStorage

您可以将其与 manage.pycollectstatic 任务一起使用。像往常一样,所有静态文件都是从您的应用程序中收集的,但此存储管理器还会创建每个文件的副本,并将 MD5 哈希值附加到名称中。例如,假设您有一个 css/styles.css 文件,它还会创建类似 css/styles.55e7cbb9ba48.css 的内容。

当然,正如您所提到的,问题在于您不希望视图和模板始终计算 MD5 哈希值以找出要生成的适当 URL。解决方案是缓存。好吧,您要求的是没有缓存的解决方案,我很抱歉,这就是为什么我说几乎。但确实没有理由拒绝缓存。 CachedStaticFilesStorage 使用名为 staticfiles 的特定缓存。默认情况下,它将使用您现有的缓存系统,瞧!但是,如果您不希望它使用常规缓存,也许是因为它是分布式内存缓存,并且您希望避免仅仅为了获取静态文件名而进行网络查询的开销,那么您可以仅为 设置特定的 RAM 缓存静态文件。这比听起来更容易:查看这个优秀的博客文章。它看起来是这样的:

CACHES = {
  'default': {
    'BACKEND': 'django.core.cache.backends.memcached.PyLibMCCache',
    'LOCATION': '127.0.0.1:11211',
  },
  'staticfiles': {
    'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
    'LOCATION': 'staticfiles-filehashes'
  }
}

Django 1.4 now includes CachedStaticFilesStorage which does exactly what you need (well... almost).

Since Django 2.2 ManifestStaticFilesStorage should be used instead of CachedStaticFilesStorage.

You use it with the manage.py collectstatic task. All static files are collected from your applications, as usual, but this storage manager also creates a copy of each file with the MD5 hash appended to the name. So for example, say you have a css/styles.css file, it will also create something like css/styles.55e7cbb9ba48.css.

Of course, as you mentioned, the problem is that you don't want your views and templates calculating the MD5 hash all the time to find out the appropriate URLs to generate. The solution is caching. Ok, you asked for a solution without caching, I'm sorry, that's why I said almost. But there's no reason to reject caching, really. CachedStaticFilesStorage uses a specific cache named staticfiles. By default, it will use your existing cache system, and voilà! But if you don't want it to use your regular cache, perhaps because it's a distributed memcache and you want to avoid the overhead of network queries just to get static file names, then you can setup a specific RAM cache just for staticfiles. It's easier than it sounds: check out this excellent blog post. Here's what it would look like:

CACHES = {
  'default': {
    'BACKEND': 'django.core.cache.backends.memcached.PyLibMCCache',
    'LOCATION': '127.0.0.1:11211',
  },
  'staticfiles': {
    'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
    'LOCATION': 'staticfiles-filehashes'
  }
}
喜爱纠缠 2025-01-09 04:31:13

我建议使用类似 django-compressor 的东西。除了自动为您处理此类内容之外,它还会自动组合和缩小您的文件以实现快速页面加载。

即使您最终没有完全使用它,您也可以检查他们的代码以获取设置类似内容的指导。它比您从简单的 StackOverflow 答案中得到的任何内容都经过了更好的审查。

I would suggest using something like django-compressor. In addition to automatically handling this type of stuff for you, it will also automatically combine and minify your files for fast page load.

Even if you don't end up using it in entirety, you can inspect their code for guidance in setting up something similar. It's been better vetted than anything you'll ever get from a simple StackOverflow answer.

佼人 2025-01-09 04:31:13

重新发明轮子并创建自己的实现有那么糟糕吗?此外,我希望低级代码(例如 nginx)在生产中为我的静态文件提供服务,而不是 python 应用程序,即使有后端也是如此。还有一件事:我希望链接在重新计算后保持不变,因此浏览器仅获取新文件。所以这是我的观点:

模板.html:

{% load md5url %}
<script src="{% md5url "example.js" %}"/>

out html:

static/example.js?v=5e52bfd3

settings.py:

STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(PROJECT_DIR, 'static')

appname/templatetags/md5url.py:

import hashlib
import threading
from os import path
from django import template
from django.conf import settings

register = template.Library()


class UrlCache(object):
    _md5_sum = {}
    _lock = threading.Lock()

    @classmethod
    def get_md5(cls, file):
        try:
            return cls._md5_sum[file]
        except KeyError:
            with cls._lock:
                try:
                    md5 = cls.calc_md5(path.join(settings.STATIC_ROOT, file))[:8]
                    value = '%s%s?v=%s' % (settings.STATIC_URL, file, md5)
                except IsADirectoryError:
                    value = settings.STATIC_URL + file
                cls._md5_sum[file] = value
                return value

    @classmethod
    def calc_md5(cls, file_path):
        with open(file_path, 'rb') as fh:
            m = hashlib.md5()
            while True:
                data = fh.read(8192)
                if not data:
                    break
                m.update(data)
            return m.hexdigest()


@register.simple_tag
def md5url(model_object):
    return UrlCache.get_md5(model_object)

注意,要应用更改后,应重新启动 uwsgi 应用程序(具体来说是进程)。

Is reinventing the wheel and creating own implementation that bad? Furthermore I would like low level code (nginx for example) to serve my staticfiles in production instead of python application, even with backend. And one more thing: I'd like links stay the same after recalculation, so browser fetches only new files. So here's mine point of view:

template.html:

{% load md5url %}
<script src="{% md5url "example.js" %}"/>

out html:

static/example.js?v=5e52bfd3

settings.py:

STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(PROJECT_DIR, 'static')

appname/templatetags/md5url.py:

import hashlib
import threading
from os import path
from django import template
from django.conf import settings

register = template.Library()


class UrlCache(object):
    _md5_sum = {}
    _lock = threading.Lock()

    @classmethod
    def get_md5(cls, file):
        try:
            return cls._md5_sum[file]
        except KeyError:
            with cls._lock:
                try:
                    md5 = cls.calc_md5(path.join(settings.STATIC_ROOT, file))[:8]
                    value = '%s%s?v=%s' % (settings.STATIC_URL, file, md5)
                except IsADirectoryError:
                    value = settings.STATIC_URL + file
                cls._md5_sum[file] = value
                return value

    @classmethod
    def calc_md5(cls, file_path):
        with open(file_path, 'rb') as fh:
            m = hashlib.md5()
            while True:
                data = fh.read(8192)
                if not data:
                    break
                m.update(data)
            return m.hexdigest()


@register.simple_tag
def md5url(model_object):
    return UrlCache.get_md5(model_object)

Note, to apply changes an uwsgi application (to be specific a process) should be restarted.

抽个烟儿 2025-01-09 04:31:13

Django 1.7 添加了 ManifestStaticFilesStorage,更好的替代 CachedStaticFilesStorage 不使用缓存系统,解决了计算哈希的问题运行时。

以下是文档的摘录:摘自文档

不推荐使用 CachedStaticFilesStorage - 几乎在所有情况下,ManifestStaticFilesStorage 都是更好的选择。使用 CachedStaticFilesStorage 时会出现一些性能损失,因为缓存未命中需要在运行时对文件进行哈希处理。远程文件存储需要多次往返才能在缓存未命中时对文件进行哈希处理,因为在嵌套文件路径的情况下需要多次文件访问才能确保文件哈希值正确。

要使用它,只需将以下行添加到 settings.py 中:

STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'

然后运行 ​​python manage.pycollectstatic;它会将 MD5 附加到每个静态文件的名称中。

Django 1.7 added ManifestStaticFilesStorage, a better alternative to CachedStaticFilesStorage that doesn't use the cache system and solves the problem of the hash being computed at runtime.

Here is an excerpt from the documentation:

CachedStaticFilesStorage isn’t recommended – in almost all cases ManifestStaticFilesStorage is a better choice. There are several performance penalties when using CachedStaticFilesStorage since a cache miss requires hashing files at runtime. Remote file storage require several round-trips to hash a file on a cache miss, as several file accesses are required to ensure that the file hash is correct in the case of nested file paths.

To use it, simply add the following line to settings.py:

STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'

And then, run python manage.py collectstatic; it will append the MD5 to the name of each static file.

初心 2025-01-09 04:31:13

您的 URL 中始终有一个带有版本的 URL 参数,并且每当您有主要版本时,您都可以更改 URL 参数中的版本。即使在 DNS 中也是如此。因此,如果 www.yourwebsite.com 加载 www.yourwebsite.com/index.html?version=1.0 那么在主要版本之后,浏览器应该加载 www. yourwebsite.com/index.html?version=2.0

我想这与您的解决方案类似 1. 您可以跟踪整个目录而不是跟踪文件吗?例如,ratehr 比 /static/style/css?v=2.0 您可以执行 /static-2/style/css 或使其更细化 /static /样式/cssv2/

How about you always have a URL Parameter in your URL with a version and whenever you have a major release you change the version in your URL Parameter. Even in the DNS. So if www.yourwebsite.com loads up www.yourwebsite.com/index.html?version=1.0 then after the major release the browser should load www.yourwebsite.com/index.html?version=2.0

I guess this is similar to your solution 1. Instead of tracking files can you track whole directories? For example ratehr than /static/style/css?v=2.0 can you do /static-2/style/css or to make it even granular /static/style/cssv2/.

烟燃烟灭 2025-01-09 04:31:13

@deathangel908 代码有更新。现在它也适用于 S3 存储(以及我认为的任何其他存储)。区别在于使用静态文件存储来获取文件内容。原始版本不适用于 S3。

应用程序名称/templatetags/md5url.py:

import hashlib
import threading
from django import template
from django.conf import settings
from django.contrib.staticfiles.storage import staticfiles_storage

register = template.Library()


class UrlCache(object):
    _md5_sum = {}
    _lock = threading.Lock()

    @classmethod
    def get_md5(cls, file):
        try:
            return cls._md5_sum[file]
        except KeyError:
            with cls._lock:
                try:
                    md5 = cls.calc_md5(file)[:8]
                    value = '%s%s?v=%s' % (settings.STATIC_URL, file, md5)
                except OSError:
                    value = settings.STATIC_URL + file
                cls._md5_sum[file] = value
                return value

    @classmethod
    def calc_md5(cls, file_path):
        with staticfiles_storage.open(file_path, 'rb') as fh:
            m = hashlib.md5()
            while True:
                data = fh.read(8192)
                if not data:
                    break
                m.update(data)
            return m.hexdigest()


@register.simple_tag
def md5url(model_object):
    return UrlCache.get_md5(model_object)

There is an update for @deathangel908 code. Now it works well with S3 storage also (and with any other storage I think). The difference is using of static file storage for getting file content. Original doesn't work on S3.

appname/templatetags/md5url.py:

import hashlib
import threading
from django import template
from django.conf import settings
from django.contrib.staticfiles.storage import staticfiles_storage

register = template.Library()


class UrlCache(object):
    _md5_sum = {}
    _lock = threading.Lock()

    @classmethod
    def get_md5(cls, file):
        try:
            return cls._md5_sum[file]
        except KeyError:
            with cls._lock:
                try:
                    md5 = cls.calc_md5(file)[:8]
                    value = '%s%s?v=%s' % (settings.STATIC_URL, file, md5)
                except OSError:
                    value = settings.STATIC_URL + file
                cls._md5_sum[file] = value
                return value

    @classmethod
    def calc_md5(cls, file_path):
        with staticfiles_storage.open(file_path, 'rb') as fh:
            m = hashlib.md5()
            while True:
                data = fh.read(8192)
                if not data:
                    break
                m.update(data)
            return m.hexdigest()


@register.simple_tag
def md5url(model_object):
    return UrlCache.get_md5(model_object)
违心° 2025-01-09 04:31:13

该解决方案的主要优点:您不必修改模板中的任何内容。

这会将构建版本添加到 STATIC_URL 中,然后网络服务器将使用 Rewrite 规则将其删除。

settings.py

# build version, it's increased with each build
VERSION_STAMP = __versionstr__.replace(".", "")
# rewrite static url to contain the number
STATIC_URL = '%sversion%s/' % (STATIC_URL, VERSION_STAMP)

所以最终的 url 例如是这样的:

/static/version010/style.css

然后 Nginx 有一个规则将其重写回 /static/style.css

location /static {
    alias /var/www/website/static/;
    rewrite ^(.*)/version([\.0-9]+)/(.*)$ $1/$3;
}

The major advantage of this solution: you dont have to modify anything in the templates.

This will add the build version into the STATIC_URL, and then the webserver will remove it with a Rewrite rule.

settings.py

# build version, it's increased with each build
VERSION_STAMP = __versionstr__.replace(".", "")
# rewrite static url to contain the number
STATIC_URL = '%sversion%s/' % (STATIC_URL, VERSION_STAMP)

So the final url would be for example this:

/static/version010/style.css

And then Nginx has a rule to rewrite it back to /static/style.css

location /static {
    alias /var/www/website/static/;
    rewrite ^(.*)/version([\.0-9]+)/(.*)$ $1/$3;
}
宫墨修音 2025-01-09 04:31:13

简单的模板标签 vstatic 创建扩展 Django 行为的版本化静态文件 url:

from django.conf import settings
from django.contrib.staticfiles.templatetags.staticfiles import static

@register.simple_tag
def vstatic(path):
    url = static(path)
    static_version = getattr(settings, 'STATIC_VERSION', '')
    if static_version:
         url += '?v=' + static_version
    return url

如果您想自动将 STATIC_VERSION 设置为当前 git 提交哈希,您可以使用以下代码片段(如有必要,请调整 Python3 代码)

import subprocess


def get_current_commit_hash():
    try:
        return subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD']).strip().decode('utf-8')
    except:
        return ''

settings.py 调用 get_current_commit_hash(),因此只会计算一次:

STATIC_VERSION = get_current_commit_hash()

Simple templatetag vstatic that creates versioned static files urls that extends Django's behaviour:

from django.conf import settings
from django.contrib.staticfiles.templatetags.staticfiles import static

@register.simple_tag
def vstatic(path):
    url = static(path)
    static_version = getattr(settings, 'STATIC_VERSION', '')
    if static_version:
         url += '?v=' + static_version
    return url

If you want to automatically set STATIC_VERSION to the current git commit hash, you can use the following snippet (Python3 code adjust if necessary):

import subprocess


def get_current_commit_hash():
    try:
        return subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD']).strip().decode('utf-8')
    except:
        return ''

At settings.py call get_current_commit_hash(), so this will be calculated only once:

STATIC_VERSION = get_current_commit_hash()
匿名。 2025-01-09 04:31:13

我在所有视图中使用全局基本上下文,其中我将静态版本设置为毫秒时间(这样,每次重新启动应用程序时它将是一个新版本):

# global base context
base_context = {
    "title": settings.SITE_TITLE,
    "static_version": int(round(time.time() * 1000)),
}

# function to merge context with base context
def context(items: Dict) -> Dict:
    return {**base_context, **items}

# view
def view(request):
    cxt = context({<...>})
    return render(request, "page.html", cxt)

我的 page.html 扩展了我的 base.html 模板,我这样使用它:

<link rel="stylesheet" type="text/css" href="{% static 'style.css' %}?v={{ static_version }}">

相当简单并且可以完成工作

I use a global base context in all my views, where I set the static version to be the millisecond time (that way, it will be a new version every time I restart my application):

# global base context
base_context = {
    "title": settings.SITE_TITLE,
    "static_version": int(round(time.time() * 1000)),
}

# function to merge context with base context
def context(items: Dict) -> Dict:
    return {**base_context, **items}

# view
def view(request):
    cxt = context({<...>})
    return render(request, "page.html", cxt)

my page.html extends my base.html template, where I use it like this:

<link rel="stylesheet" type="text/css" href="{% static 'style.css' %}?v={{ static_version }}">

fairly simple and does the job

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文