🤌 几分钟搞清 Django 静态文件处理
date
Nov 13, 2023
slug
django-static-quick-glance
status
Published
tags
python
django
summary
几分钟就能拿捏
type
Post
Django 处理静态文件有一套 Static 机制,它是简单的,但有时又是烦人的。
首先,啥是静态文件?不需要服务端计算,直接页面上请求拉取的文件:图片、视频、html、JS、CSS 等等。理论上来说,既然不需要计算,那实际上和 Python 也没啥必然联系了,所以 Django 在这里的职责就是“帮忙托管一下”。
说是帮忙,是因为托管静态文件这件事本来就有更专业的组件负责,比如 Nginx、CDN 等等。
DEBUG 一时爽
如果你还在本地开发阶段,可能觉得下面的内容都挺奇怪,明明不用额外配置就能访问所有静态文件。是的,当
DEBUG=True
的时候,Django 基本都帮你包办了,托管给 WhiteNoise 也会自动刷新静态文件。但如果放到生产,Django 就不再帮忙了,WhiteNoise 也只在程序启动时加载时,以下的内容就需要你自己梳理清楚了。Django 怎么代管
所谓的帮忙代管,总结起来就是三个步骤:
- 你要告诉 Django:来自各个不同来源的静态文件分别放在什么位置,比如 Django 模版、前后端分离时的前端独立工程、项目外的视频图片等等。(对应 Django 配置
STATICFILES_DIRS
)
- 你要告诉 Django:最终你想把这些静态文件放到什么位置,这个位置可以就是专业组件能找到的位置。(对应 Django 配置
STATIC_ROOT
)
- 执行 Django 命令:
python manage.py collectstatic
,Django 会忠实地按照你提供的信息把分散的静态文件收集放到同一个位置。
这三步搞定,Django 静态文件就处理完了,这些文件怎么被访问到,那就是专业组件的事情了。好奇宝宝们肯定有很多问题想问 Django:
- 静态文件来源五花八门,怎么处理?
STATICFILES_FINDERS
实现自定义FINDER
逻辑
- 静态文件怎么直接放云上 CDN 里去?或者远端 Nginx ?
STATICFILES_STORAGE
实现自定义STORAGE
逻辑
回答完这些问题,Django 双手一摊:“静态文件没我的事了吧” 🤷♂️。
等等,你这个机灵鬼,我还没说完呢。
让 Django 多管点“闲事”
对于请求量大、地域来源广的服务,将站点的静态文件放到专业组件托管肯定是更好的选择。但有时候我们也需要部署一些体量不大应用——例如一些在公司内部服务的中型 SaaS,这时候把静态文件分离管理反而会增加流程和架构的复杂性(除非有非常完善的基建支持),仅仅是负责静态文件的分发是远远不够的,所以使用 Django 代理静态文件的请求也是很多生产环境的选择。
那么谁来做这部份工作呢?专业组件的工作 Django 看了直摆手,这时,【白噪声同志】应声而来。
在安装 WhiteNoise 之后,它将在你的服务内部帮忙
serve
静态文件,代理这些文件的请求以及相关的事务处理(压缩、缓存等等)。相关配置直接看官方文档就好,这里就不赘述了。这么看起来,问题好像就直接解决了?等等,烦人的东西才刚来。
烦人的子路径问题
刚才通过 WhiteNoise 模块,已经将静态文件的访问入口收归到 Django 服务中了,理想状态下,直接访问
https://amazing-fake-app/static/
(通过 Django STATIC_URL
确定访问路径)就能寻找到收集好的静态文件了。但是很多时候我们的服务并不是直接暴露在域名下的,往往会有“访问路径”作为前缀,例如服务本身访问:
https://amazing-fake-app/foo/
,这时如果保持原来的 STATIC_URL='/static'
很有可能就有问题。看起来这应该是 Django 应该处理的东西,为什么会有问题呢?首先,
/foo/
通常是由外部接入层(例如 Nginx )转发提供的,Django 理论上是可以感知到它的存在:def get_script_name(environ):
"""
Return the equivalent of the HTTP request's SCRIPT_NAME environment
variable. If Apache mod_rewrite is used, return what would have been
the script name prior to any rewriting (so it's the script name as seen
from the client's perspective), unless the FORCE_SCRIPT_NAME setting is
set (to anything).
"""
if settings.FORCE_SCRIPT_NAME is not None:
return settings.FORCE_SCRIPT_NAME
# If Apache's mod_rewrite had a whack at the URL, Apache set either
# SCRIPT_URL or REDIRECT_URL to the full resource URL before applying any
# rewrites. Unfortunately not every Web server (lighttpd!) passes this
# information through all the time, so FORCE_SCRIPT_NAME, above, is still
# needed.
script_url = get_bytes_from_wsgi(environ, 'SCRIPT_URL', '') or get_bytes_from_wsgi(environ, 'REDIRECT_URL', '')
if script_url:
if b'//' in script_url:
# mod_wsgi squashes multiple successive slashes in PATH_INFO,
# do the same with script_url before manipulating paths (#17133).
script_url = _slashes_re.sub(b'/', script_url)
path_info = get_bytes_from_wsgi(environ, 'PATH_INFO', '')
script_name = script_url[:-len(path_info)] if path_info else script_url
else:
script_name = get_bytes_from_wsgi(environ, 'SCRIPT_NAME', '')
return script_name.decode()
可以看到 Django 一共可以在三个地方感知到子路径的存在:
- 用户直接通过
FORCE_SCRIPT_NAME
手动设置
- 从 wsgi 信息中获取
SCRIPT_URL
或REDIRECT_URL
,再转换成SCRIPT_NAME
- 从 wsgi 信息中获取
SCRIPT_NAME
第一个手动设置的我们先不讨论;第二个在代码和注释中说的很清楚了:特定的接入层(例如 Apache )会稳定提供,但不一定能保证;第三个看起来是缺省的获取方法,理论上它应该能覆盖大部份场景才对。
然而,假如你的接入层是广泛使用的 Nginx,缺省方法就有问题了:默认地,Nginx 并不支持在 HTTP Header 中使用包含下划线的变量,也就说
SCRIPT_NAME
默认不合法。- 如果你可以修改接入层的配置:可以手动调整
underscores_in_headers
的配置,但不幸的是,Nginx 还有个默认的行为,给 Header 添加HTTP_
,也就是只能通过变量HTTP_SCRIPT_NAME
获取,Django 再次 Miss。你可以进一步地修改 Nginx 配置,直接手动添加SCRIPT_NAME
:`uwsgi_param SCRIPT_NAME $http_script_name;
- 如果你不能手动修改接入层的配置:刚才略过的手动设置
FORCE_SCRIPT_NAME
就派上用场了,手动添加上FORCE_SCRIPT_NAME=/foo/
当然你可以在 Django wsgi 加载时做一个动态的配置,确保接入层的变更能及时更新到配置中:
class DynamicScriptNameWSGIHandler(WSGIHandler):
def __call__(self, environ, start_response):
# HTTP_X_SCRIPT_NAME(from Nginx) -> SCRIPT_NAME(Django wsgi)
script_name = environ.get('HTTP_X_SCRIPT_NAME')
if script_name is not None:
environ['SCRIPT_NAME'] = script_name
settings.FORCE_SCRIPT_NAME = settings.SITE_URL = '%s/' % script_name
return super().__call__(environ, start_response)
至此,静态文件处理的流程就很清楚了,挺简单的一件事,就几分钟搞定,打完收工。