为什么Django没有正确提供我的SPA静态文件?
我正在用 Django 做网站的后台,用 Vuejs 做前台。在开发的时候,我分别用 python manage.py runserver
和 yarn serve
启动了后台和前台,效果很好。现在我想把网站上线。为此,我运行了 yarn build
,这在我的前台文件夹里创建了一个 dist/
文件夹。我的文件结构大概是这样的:
cockpit
├── backend/
│ ├── cockpit/
│ │ ├── views.py
│ │ ├── css/
│ │ └── etc..
│ ├── settings/
│ │ └── settings.py
│ └── manage.py
└── frontend/
└── dist/
├── index.html
├── css/
└── js/
现在我想让我的 Django 项目能提供 frontend/dist/
里的文件,这样我就可以用 uwsgi 来运行一切。为此,我在尝试按照 这篇文章的说明来做。我在 settings/urls.py
里有以下内容:
from django.contrib import admin
from django.urls import include, path, re_path
from django.views.generic import TemplateView
urlpatterns = [
path('admin/', admin.site.urls),
path('cockpit/', include("cockpit.urls")),
re_path('', TemplateView.as_view(template_name='index.html')),
]
并在我的 settings.py 里设置了以下配置:
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': ['static'], # <== ADDED THIS
'APP_DIRS': True,
'OPTIONS': {
# removed to keep this example small
},
},
]
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
STATIC_URL = '/static/'
STATICFILES_DIRS = [
os.path.join(BASE_DIR, '../frontend/dist'),
]
STATIC_ROOT = os.path.join(BASE_DIR, "static/")
print("BASE_DIR:", BASE_DIR)
print("STATIC_ROOT:", STATIC_ROOT)
print("STATICFILES_DIRS:", STATICFILES_DIRS)
然后打印出来的结果是这样的:
BASE_DIR: /home/kramer65/repos/cockpit/backend
STATIC_ROOT: /home/kramer65/repos/cockpit/backend/static/
STATICFILES_DIRS: ['/home/kramer65/repos/cockpit/backend/../frontend/dist']
接着我运行了 `python manage.py collectstatic`:
$ python manage.py collectstatic
150 static files copied to '/home/kramer65/repos/cockpit/backend/static'.
现在看起来是这样的:
cockpit
├── backend/
│ ├── cockpit/
│ │ ├── views.py
│ │ ├── css/
│ │ └── etc..
│ ├── settings/
│ │ └── settings.py
│ └── manage.py
│ └── static/
│ ├── index.html
│ ├── css/
│ └── js/
└── frontend/
└── dist/
├── index.html
├── css/
└── js/
我通过在 backend/static/
文件夹里运行 (node) http-server
来测试它。在浏览器里,网站加载得很好,运行得也不错。下面是命令行的输出:
$ http-server
Starting up http-server, serving ./
Available on:
http://127.0.0.1:8080
http://192.168.0.104:8080
Hit CTRL-C to stop the server
[2020-05-18T13:50:58.487Z] "GET /" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:76.0) Gecko/20100101 Firefox/76.0"
(node:5928) [DEP0066] DeprecationWarning: OutgoingMessage.prototype._headers is deprecated
[2020-05-18T13:50:58.671Z] "GET /css/chunk-vendors.2c7f3eba.css" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:76.0) Gecko/20100101 Firefox/76.0"
[2020-05-18T13:50:58.679Z] "GET /css/app.e15f06d0.css" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:76.0) Gecko/20100101 Firefox/76.0"
[2020-05-18T13:50:58.681Z] "GET /js/chunk-vendors.9c409057.js" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:76.0) Gecko/20100101 Firefox/76.0"
[2020-05-18T13:50:58.687Z] "GET /js/app.c930fce5.js" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:76.0) Gecko/20100101 Firefox/76.0"
我停止了这个 http-server
,启动了 Django 开发服务器,然后打开了浏览器。终端显示了这个:
$ python manage.py runserver
Watching for file changes with StatReloader
Performing system checks...
System check identified no issues (0 silenced).
May 18, 2020 - 17:57:00
Django version 3.0.6, using settings 'settings.settings'
Starting ASGI/Channels version 2.4.0 development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
HTTP GET / 200 [0.22, 127.0.0.1:33224]
HTTP GET /static/debug_toolbar/css/print.css 200 [0.04, 127.0.0.1:33232]
HTTP GET /static/debug_toolbar/css/toolbar.css 200 [0.05, 127.0.0.1:33234]
HTTP GET /static/debug_toolbar/js/toolbar.js 200 [0.02, 127.0.0.1:33232]
HTTP GET /static/debug_toolbar/js/toolbar.timer.js 200 [0.04, 127.0.0.1:33234]
HTTP GET /js/chunk-vendors.9c409057.js 200 [0.80, 127.0.0.1:33228]
HTTP GET /css/chunk-vendors.2c7f3eba.css 200 [0.94, 127.0.0.1:33224]
HTTP GET /js/app.c930fce5.js 200 [0.98, 127.0.0.1:33230]
HTTP GET /css/app.e15f06d0.css 200 [0.99, 127.0.0.1:33226]
HTTP GET /favicon.ico 200 [0.09, 127.0.0.1:33226]
在浏览器控制台里,我看到资源似乎被加载了,但有些文件是空的(0 字节),屏幕上什么也不显示。下面是结果的截图,以及 Django 调试栏里的静态文件标签的截图。
为什么在 Django 中这些文件没有正确提供呢?
编辑
我刚发现,如果我把
STATIC_URL = '/static/'
改成
STATIC_URL = '/'
当我访问 http://127.0.0.1:8000/index.html
时,它能正常工作,但现在访问 http://127.0.0.1:8000/
会给我这个错误:
[编辑 2]
好的,按照 @geek_life 的建议,我把设置文件里的值改成了:
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
PROJECT_NAME = 'cockpit'
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, PROJECT_NAME, "static/")
STATICFILES_DIRS = [os.path.join(BASE_DIR, '../frontend/dist')]
print("## BASE_DIR:", BASE_DIR)
print("## STATIC_ROOT:", STATIC_ROOT)
print("## STATICFILES_DIRS:", STATICFILES_DIRS)
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': ['cockpit/static/'], # <= THIS IS WHAT I CHANGED
'APP_DIRS': True,
'OPTIONS': {} # And here some options
},
]
这打印出来的结果是:
## BASE_DIR: /home/kramer65/repos/cockpit/backend
## STATIC_ROOT: /home/kramer65/repos/cockpit/backend/cockpit/static/
## STATICFILES_DIRS: ['/home/kramer65/repos/cockpit/backend/../frontend/dist']
在 settings/urls.py
文件里,我(仍然)得到了这个:
urlpatterns = [
path('admin/', admin.site.urls),
path('cockpit/', include("cockpit.urls")),
re_path('', TemplateView.as_view(template_name='index.html')),
]
然后我把从 cockpit/backend/
里构建的 vuejs-ap 的 static/
文件夹复制到了 cockpit/backend/cockpit/
。
不幸的是,我仍然得到了相同的结果。index.html 能加载,但 JSS 和 CSS 文件还是不能。接下来我该尝试什么呢?
相关问题:
- 暂无相关问题
6 个回答
在你开始搭建前端之前,把 css/ 和 js/ 文件放到一个叫做 'static' 的文件夹里,保持 index.html 文件不变。然后,当你运行 yarn build
的时候,你的文件结构会变成下面这样 -
cockpit
├── backend/
│ ├── cockpit/
│ │ ├── views.py
│ │ ├── css/
│ │ └── etc..
│ ├── settings/
│ │ └── settings.py
│ └── manage.py
└── frontend/
└── dist/
├── static/
│ ├── css/
│ └── js/
└── index.html
设置你的 BASE_DIR 和 FRONTEND_DIR,指向 'cockpit/frontend/dist',这样以后使用起来会方便很多。
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
FRONTEND_DIR = os.path.join(os.path.dirname(BASE_DIR),'frontend','dist')
现在,你可以像之前那样把前端的静态文件复制到后端目录 -
STATIC_URL = '/static/'
STATICFILES_DIRS = [
os.path.join(FRONTEND_DIR,'static/'),
]
STATIC_ROOT = os.path.join(BASE_DIR, "static/")
或者,你也可以直接让后端从构建好的前端目录提供服务,而不是再复制一遍。(我总是这样做)
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(FRONTEND_DIR,'static/')
把你构建好的前端目录包含在模板目录里,这样 Django 才能提供 index.html
文件。
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [FRONTEND_DIR],
'APP_DIRS': True,
'OPTIONS': {
# removed to keep this example small
},
},
]
现在,运行你的服务器。
$ python manage.py collectstatic
$ python manage.py runserver
如果你之后决定把这个项目上线,你可以很简单地使用这个配置,通过设置 Nginx 来从你的静态目录提供静态文件。
希望这对你有帮助 :)
在你的 [EDIT] 中澄清你的疑惑:
STATIC_ROOT 指向你系统中的一个目录。STATIC_URL 是你在使用网站时访问 STATIC_ROOT 中静态文件的 URL。
假设你在目录中有一个文件 script.js,如下所示:
cockpit
├── backend/
│ ├── cockpit/
│ │ ├── views.py
│ │ ├── css/
│ │ └── etc..
│ ├── settings/
│ │ └── settings.py
│ └── manage.py
└── frontend/
└── dist/
├── static/
│ ├── css/
│ └── js/
│ └── script.js
└── index.html
而你把 STATIC_ROOT 设置为指向 'cockpit/frontend/dist/static/'。
现在,如果你把 STATIC_URL 设置为 'static/',你就可以通过 http://127.0.0.1:8000/static/js/script.js 来访问这个脚本。
另外,如果你把 STATIC_URL 设置为 'xcsdf/',你就可以通过 http://127.0.0.1:8000/xcsdf/js/script.js 来访问你的 js 文件。
首先要说的是!如果你想用Django来提供用户界面(UI)的静态文件用于生产环境,这其实不是个好主意!你应该考虑使用nginx或者类似的网络服务器来为你提供静态文件。
在你的urls.py
文件中使用的TemplateView
有点问题。因为在你第一个场景中,静态文件是通过STATIC_URL = '/static/'
来提供的,所以js和css文件的链接不匹配。你可能会编辑文件,在index.html
中放入static
模板标签,但这也不是个好主意。当你在vuejs那边做了更改后,你需要直接构建并替换包,这样你的网站才能更新。所以这行代码需要永久删除,index.html
必须被当作静态文件处理:
re_path('', TemplateView.as_view(template_name='index.html')),
当你把STATIC_URL = '/'
改了之后,你就可以访问所有的静态文件了。如果你在debug = True
模式下运行django,它会自动提供静态文件。否则,你需要在文件中添加静态url配置。(记住,这在生产环境中是不好的!)
from django.contrib import admin
from django.urls import include, path, re_path
from django.views.generic import TemplateView
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
path('admin/', admin.site.urls),
path('cockpit/', include("cockpit.urls")),
]
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
使用这种设置时,你需要小心你的url和静态文件的命名。它们可能会互相覆盖。
附言:谢谢你提出这么详细的问题。
我很久以前遇到过这个问题,后来用一个简单的方法解决了。真的很简单。 我看到你的静态文件夹不在应用的根目录,而是在错误的位置。 把它放到你的应用根目录下……因为Django会在你的应用的主根目录里寻找静态文件夹,那个地方就是views.py所在的地方。 也许你有五个或更多的应用。 但Django并不关心你有多少个应用,它只会在你的应用根目录里寻找静态文件夹。 不过,你的模板文件夹可以放在项目的根目录下。 所以,把你的静态文件夹放到应用的根目录里。 在这里,我指的是cockpit的根目录。
然后你需要在settings.py文件中添加这些更改。
PROJECT_NAME = '---Your projects name---'
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, PROJECT_NAME, 'static/')
EXTERNAL_APPS = [
'django.contrib.staticfiles',
]
祝你好运。
关于 STATIC_URL
的使用
首先,STATIC_URL
是用来指定静态文件的地址的地方。比如,如果你设置 STATIC_URL='/'
,然后访问 http://127.0.0.1:8000/index.html
,这实际上会从你的 Django 应用的静态文件夹中提供 index.html
文件。如果你直接访问 http://127.0.0.1:8000/
,那你就到了“静态文件夹的根目录”,但这时候会发现目录索引是禁止的。所以,不建议使用 STATIC_URL='/'
。
文件的位置
你跟随的教程会把单页面应用(SPA)的构建文件夹添加到 Django 的 settings.py
文件中的模板里。
所以你应该尝试添加:
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(os.path.dirname(BASE_DIR), 'frontend', 'dist')], # <== ADDED THIS
'APP_DIRS': True,
'OPTIONS': {
# removed to keep this example small
},
]
或者如果你想把静态文件作为模板目录来指向:
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'static')],
'APP_DIRS': True,
'OPTIONS': {
# removed to keep this example small
},
]
不过我觉得这样做不是最佳实践,因为静态文件(比如图片、JS、CSS)在 Django 中有自己的管理方式。
关于 os.path
的说明
另外,关于你的 STATICFILES_DIRS
,使用 os.path.join()
的方式并不是这样。第一个参数应该是你想要连接的路径,后面的路径部分作为下一个参数传入。而且,BASE_DIR
的目录层级不对,你可以通过打印 BASE_DIR
的路径来看到。你需要的是 BASE_DIR
目录的上级目录。要获取 上级目录 的路径,你可以使用 os.path.dirname(BASE_DIR)
,因为 BASE_DIR
是一个路径。这样应用后,我们得到:
STATICFILES_DIRS = [
os.path.join(os.path.dirname(BASE_DIR), frontend, dist),
]
最后的说明
我觉得让 Django 提供单页面应用(SPA)而不把它完全放在 Django 的 BASE_DIR
中,可能不是个好做法(所以叫做基础目录)。不过我不太清楚具体的来源,或者这是否真的是最佳实践。
错误原因
对于第一个情况
STATIC_URL = '/static/'
Django 只会在 backend/static/
文件夹中查找静态文件,前提是你的 index.html
中提到了带有 /static/
的网址,比如 /static/css/*
或 /static/js/*
。但在这里,index.html
中引用的文件是 /css/*
和 /js/*
,所以找不到这些文件。
这个情况在博客示例中能正常工作,是因为他们的模板文件放在 '../frontend/build'
目录下,而静态文件在 '../frontend/build/static'
中。因此,index.html
会查找 static/js/*
而不是 /js/*
,这样 Django 就能正确地在 backend/static
中找到文件。
所以在你的代码中,如果设置成第二种情况,也就是:
STATIC_URL = '/'
就能正确获取静态文件的地址 /css/*
和 /js/*
,甚至你的 /index.html
也可以被看作是一个静态文件。也就是说,所有这些网址都被视为静态的,并且会在 backend/static/
文件夹中进行查找,因此在浏览器中能正确显示。
但是现在网址搞混了,也就是这个:
re_path('', TemplateView.as_view(template_name='index.html')),
可以被理解为 Django 在查找这个位置:/
,但这个位置已经被保留用于查找静态文件,而在 /
后面不加任何文件名意味着你并没有在查找任何文件。
可能的解决方案
Django 允许自定义网址模式,也就是说你可以在 settings.py 中创建两个新变量:
STATIC_URL = "/static/"
STATIC_ROOT = os.path.join(BASE_DIR, "static/")
STATIC_JS_URL = "/js/"
STATIC_JS_ROOT = os.path.join(STATIC_ROOT, "js/")
STATIC_CSS_URL = "/css/"
STATIC_CSS_ROOT = os.path.join(STATIC_ROOT, "css/")
然后配置你的 urls.py:
from django.contrib import admin
from django.urls import include, path, re_path
from django.views.generic import TemplateView
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
path('admin/', admin.site.urls),
path('cockpit/', include("cockpit.urls")),
re_path('', TemplateView.as_view(template_name='index.html')),
]
urlpatterns += static(settings.STATIC_JS_URL, document_root=settings.STATIC_JS_ROOT)
urlpatterns += static(settings.STATIC_CSS_URL, document_root=settings.STATIC_CSS_ROOT)