目录
前言
刚学习Django不久,这两天搭建环境时遇到一个问题,项目静态文件在 settings.py 里 DEBUG = True的情况下,一切正常,运行的项目所有资源也正常加载,但是在 DEBUG = False 的情况下,就会出问题,资源都不存在。
这篇文章就刚刚出现的问题,深究一下Django的文件加载机制和给出解决方案。
一些基本概念
想要使用Django的文件系统,我们需要对 settings.py 中文件相关的配置做一些了解:
-
在 INSTALLED_APPS 里添加 django.contrib.staticfiles 模块,这个模块提供了对静态文件的支持。
-
STATIC_ROOT
默认: Nonecollectstatic 将收集静态文件进行部署的目录的绝对路径。
例如: “/var/www/example.com/static/”
如果 staticfiles contrib 应用已启用(如在默认的项目模板中), collectstatic 管理命令将收集静态文件到这个目录。
-
STATIC_URL
默认: None引用位于 STATIC_ROOT 中的静态文件时要使用的 URL。
例如: “/static/” 或 “http://static.example.com/”
如果不是 None,这将被用作 资源定义 (Media 类)和 静态文件应用 的基本路径。
如果设置为非空值,必须以斜线结束。
-
STATICFILES_DIRS
默认: [] (空列表)这个配置定义了静态文件应用在启用 FileSystemFinder 查找器时将穿越的额外位置,例如,如果你使用 collectstatic 或 findstatic 管理命令或使用静态文件服务视图。
这应该被设置为包含附加文件目录完整路径的字符串列表,例如:
STATICFILES_DIRS = [ "/home/special.polls.com/polls/static", "/home/polls.com/polls/static", "/opt/webfiles/common", ]
请注意,这些路径应该使用 Unix 风格的斜线,即使在 Windows 上也是如此(例如 “C:/Users/user/mysite/extra_static_content”)。
注意:
-
开发环境一般需要配置的是STATICFILES_DIRS,指向我们静态文件存放的目录,不然项目可能找不到我们放置的静态文件资源
-
STATIC_URL 的作用主要体现在使用官方推荐的方式加载资源时,提供url的前缀,开发环境不太在意,但也需要配置。
-
STATIC_ROOT ,这也是一个重要或者又不重要的配置,他的主要作用体现在你需要部署项目是,根据这个配置,把项目里的静态文件收集到这个目录里,主要用在每个APP里都有各自的静态文件存放目录,那么这个配置就有必要。如果都在最外部,那就根本没有执行收集的必要,直接使用哪个文件就行了。
问题还原
这里给出出现问题时的代码
1. 第一种(Django推荐方式)
- test.js
function showAlert() { alert("测试弹窗") }
- test.css
p { color: red; font-size: 32px; }
- index.html
{% load static %} <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> <link rel="stylesheet" type="text/css" href="{% static "css/test.css" %}"> <script src="{% static "js/test.js" %}"></script> </head> <body> <p>这是一段文字...</p> <br> <br> <img src="{% static "images/cat.png" %}" alt="runoob-logo" width="100"> <br> <br> <button onclick="showAlert()">按钮</button> </body> </html>
其实你会发现,加载静态资源时,Django推荐的也是这种写法,当我们在DEBUG = True 时,他工作良好,渲染界面如下:
但是一旦当我们设置 DEBUG = False ,你就会发现, img、css、和js都加载失败,渲染界面如下:
可见三者皆加载失败,其中css报的错可能不一样,但是你把他的MIME type修改了也是一样的,这里就不展示了。
到这里了,肯定就觉得是有问题,但先不管它,我们换一种方式看看行不行
2. 第二种(相对路径)
这二种的话我第一时间想到的就是相对路劲,相对路径应该就可以了吧,当我们使用其他语言框架开发的时候,相对路径总是一种引用外部资源文件的方式,下面列出代码(仅修改html):
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="stylesheet" type="text/css" href="../../static/css/test.css">
<script src="../../static/js/test.js"></script>
</head>
<body>
<p>这是一段文字...</p>
<br>
<br>
<img src="../../static/images/cat.png" alt="runoob-logo" width="60">
<br>
<br>
<button onclick="showAlert()">按钮</button>
</body>
</html>
到此你可能以为大功告成了,不过很遗憾的告诉你,页面呈现出来的结果和 第一种 是一模一样的,具体渲染效果参见 第一种。
到此,结果很明显了,上诉都行不通,下面我们就来开始探究原因和解决上面的问题吧。
原因分析
由上面的失败案例可知,其实为什么加载失败,就是找不到资源,为什么找不到资源呢,很简单啊,资源路径没定义,你可能也发现了,Django创建的项目都会有个 urls.py 的文件,这个文件的默认内容一般如下:
from django.contrib import admin
from django.urls import path
urlpatterns = [
path('admin/', admin.site.urls),
]
如果你的项目包含Django的权限模型系统的话,会有一个admin/ 的默认配置连接,如果创建了一个APP的话应该就会配置APP的路径,结构也不过大致是这样:
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('app/', include('app.urls')),
]
好吧,可见并没有任何关于静态文件的urls的定义。
和其他开发框架不一样的是,Django 这个框架对未定义urls的资源,是不能访问的。
到此,你大致知道了为什么 资源路径不能访问了吧,下面就来解决问题。
解决方式
第一种方式(自己定义url)
说实话,官方不推荐这种解决方式,这里只是给个例子。
需要注意的是,这种方式需要我们在settings.py里的 INSTALLED_APPS 里添加 django.contrib.staticfiles 模块,这个是核心。
这个模块当中封装的就是静态文件加载的功能,然后阅读官方文档staticfiles 应用,其中有一点提到了 静态文件开发视图 的功能,下面是代码:
from django.conf import settings
from django.contrib.staticfiles import views
from django.urls import re_path
if settings.DEBUG:
urlpatterns += [
re_path(r'^static/(?P<path>.*)$', views.serve),
]
注意这里使用re_path这个API(正则)加载url,还有注意这里的re_path里的static这个路劲一定要和你 settings.py里定义的 STATIC_URL的名称一致,具体原因见 一些基本概念,也可看官方文档。
有了上面的代码,我们大致有了一些思路了,当然需要做些修改,我们肯定是想在 DEBUG = False 里使用静态文件的,所以必要去除的就是 **if settings.DEBUG:**这个条件,当然,仅这样肯定也是不行的,我们看见这里这个url指向的是 django.contrib.staticfiles 里的 serve视图,我们进去看看,下面是源码:
import os
import posixpath
from django.conf import settings
from django.contrib.staticfiles import finders
from django.http import Http404
from django.views import static
def serve(request, path, insecure=False, **kwargs):
if not settings.DEBUG and not insecure:
raise Http404
normalized_path = posixpath.normpath(path).lstrip('/')
absolute_path = finders.find(normalized_path)
if not absolute_path:
if path.endswith('/') or path == '':
raise Http404("Directory indexes are not allowed here.")
raise Http404("'%s' could not be found" % path)
document_root, path = os.path.split(absolute_path)
return static.serve(request, path, document_root=document_root, **kwargs)
好吧,这里面也有个判断,故此要么我们修改源码,要门就需要自己重新提供一个模板了,修改源码肯定不现实,你总不可能往一台服务器或者需要调试的机器就要求别人修改源码吧,所以我们就来自己提供模板吧。
首先需要做的就是在我们的初始项目里添加一个view_url.py 的文件,类似这样(演示这样写,你可以定义到别的地方):
在这个文件里面复制上面的源代码内容,注释 **if not settings.DEBUG and not insecure:**这个条件:
import os
import posixpath
from django.conf import settings
from django.contrib.staticfiles import finders
from django.http import Http404
from django.views import static
def serve(request, path, insecure=False, **kwargs):
# if not settings.DEBUG and not insecure:
# raise Http404
normalized_path = posixpath.normpath(path).lstrip('/')
absolute_path = finders.find(normalized_path)
if not absolute_path:
if path.endswith('/') or path == '':
raise Http404("Directory indexes are not allowed here.")
raise Http404("'%s' could not be found" % path)
document_root, path = os.path.split(absolute_path)
return static.serve(request, path, document_root=document_root, **kwargs)
ok,这个文件配置完了,接下来修改同级目录下的urls.py如下:
from django.conf import settings
from django.contrib import admin
from django.urls import path, include, re_path
from .view_url import serve
urlpatterns = [
path('cp/admin/', admin.site.urls),
path('cp/app/', include('app.urls')),
]
if not settings.DEBUG:
urlpatterns += [
re_path(r'^cp/static/(?P<path>.*)$', serve),
]
注意:
- 仅在settings.DEBUG 为 False 时,修改urlpatterns,本身在DEBUG=True的时候就不需要配置,这里就不要重复设置了。
- 在我们的settings.py里项目本身配置了一些中间件,其中一个中间件配置了一个参数(即MIDDLEWARE->django.middleware.security.SecurityMiddleware)中间件,查看源码在process_response函数里可看到如下配置:
这个header头的作用可查看X-Content-Type-Options介绍,所以我们在urls.py的统计目录的__init__.py文件里添加如下配置即可:if self.content_type_nosniff: response.headers.setdefault('X-Content-Type-Options', 'nosniff')
为什么加这个,就在我们上面创建的文件view_url.py里,也可看源码,最后一行放回的是:static.serve(…),即在django.views模块里,我们看下他的serve源码:import mimetypes mimetypes.add_type("text/css", ".css", True) mimetypes.add_type("text/javascript", ".js", True)
其中 使用 mimetypes.guess_type这个模块的函数返回文件的类型,但是其中可能并没有定义相关的类型,比如js,所以我们需要扩展它,即上面的 init.py 的代码。def serve(request, path, document_root=None, show_indexes=False): path = posixpath.normpath(path).lstrip('/') fullpath = Path(safe_join(document_root, path)) if fullpath.is_dir(): if show_indexes: return directory_index(path, fullpath) raise Http404(_("Directory indexes are not allowed here.")) if not fullpath.exists(): raise Http404(_('“%(path)s” does not exist') % {'path': fullpath}) # Respect the If-Modified-Since header. statobj = fullpath.stat() if not was_modified_since(request.META.get('HTTP_IF_MODIFIED_SINCE'), statobj.st_mtime, statobj.st_size): return HttpResponseNotModified() content_type, encoding = mimetypes.guess_type(str(fullpath)) content_type = content_type or 'application/octet-stream' response = FileResponse(fullpath.open('rb'), content_type=content_type) response.headers["Last-Modified"] = http_date(statobj.st_mtime) if encoding: response.headers["Content-Encoding"] = encoding return response
到此第一种方式就结束了,上面的url的配置具体可根据自己的项目结构配置,这里就不多说了,下面我们设置 settings.py 的 DEBUG=False,运行项目,渲染结果如下:
OK,到这里,这种方式就解决了,当然官方不推荐这种方式,还是推荐我们自己搭个静态资源服务器。
第二种方式(直接使用静态资源服务器路径)
说实话,这种应该是最常用的,我们直接配置资源url为一个提供服务的静态资源服务,
使用这种方式的话,我们需要把项目所有的静态文件,打包到一起,可以使用Django提供的方式:
python manage.py collectstatic
这样会把项目配置的所有静态资源全家加载到
STATIC_ROOT = '/static/'
这里指向的位置,文件会拷贝进去,然后直接把这个文件夹放到静态服务器上即可。
这种方式的话资源和项目可不再同个服务器,当然一个也没事,而且也可以使用cdn之类的服务器等等,下面是个nginx的配置(仅测试):
location /static {
alias /statics;
allow all;
autoindex on;
}
然后修改我们项目的settings.py文件,修改其中的 STATIC_URL配置,指向我们nginx的服务器的静态资源路径,类似如下:
STATIC_URL = 'http://liuhuan.shop/static/'
其实本地调试也行,你把这个指向一个随便的本地连接,当然需要个可提供服务的东西,nginx、IIS、 tomcat、node等等,此后我们去除第一种方式当中的代码,或者初始化个新项目,注意,设置DEBUG=False和上面的STATIC_URL ,下面看下渲染结果,注意HTML的代码不变,类似js的还是如下:
<script type="text/javascript" src="{% static "/js/test.js" %}"></script>
其中static 会使用 settings.py的 STATIC_URL替换,结果就是http://liuhuan.shop/static/js/test.js 之类的,渲染如下。
第三种方式(本地服务器)
这种方式和第二种是一样的,只不过这个在本地调试的有限制,settings.py的 STATIC_URL配置如下:
STATIC_URL = '/cp/static/'
这个配置还是看具体情况,但是当我们这样配的时候,本地调式结果是不会出来的,因为你本地运行项目的时候,项目会占用一个端口,类似:http://127.0.0.1:8000/,而你STATIC_URL 配置的话,项目运行时静态资源加载的资源可能会是 http://127.0.0.1:8000/cp/static/js/test.js 这个路径,端口和项目是一致的,但是你本地配置的一个静态资源服务器,端口肯定和这个不一致,就算你配置的服务器路径在cp/static/js/test.js有资源,但是也加载不出来,端口不一致嘛,所以一般的话本地要调式,需要安装UWSGI,然后静态资源和项目都使用nginx转发,这样内部资源加载时,都是走nginx转发,域名就一致了,肯定就能加载出来了,放到发布的服务器肯定就没问题了,都是走域名访问的嘛。
结语
到此,三种方式就介绍完了,对了,推荐大家看下 django.contrib.staticfiles 的源码,静态文件的处理都放在这里面的。
其实,到现在,大家应该清楚了,资源为什么加载不出来,就是资源访问不到,为什么访问不到,项目没有提供对哪个链接的路径,那么,解决思路也很简单,给项目提供哪个链接即可,类似 第一种方式,我自己维护可以了吧,可以。
当然使用django.contrib.staticfiles 提供的代码可以,但是我们自己实现也行,反正理解就是提供一个url,然后根据这个url,解析成获得本地的资源,再把这个资源返回去就行,这个不算难,大家自有发挥。
但是Django不推荐这种方式,推崇的是第二种,第三种和第二种是一致的,思路都是提供资源的服务器,我那个链接访问不到,我就添加哪个服务器,提供一个环境给它嘛。
现在只是浅尝则之,毕竟刚接触,后面有时间或者更好的方式,再修改吧。