为什么Django没有正确提供我的SPA静态文件?

13 投票
6 回答
1407 浏览
提问于 2025-06-18 03:59

我正在用 Django 做网站的后台,用 Vuejs 做前台。在开发的时候,我分别用 python manage.py runserveryarn 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 中这些文件没有正确提供呢?

enter image description here

enter image description here

编辑

我刚发现,如果我把

STATIC_URL = '/static/'

改成

STATIC_URL = '/'

当我访问 http://127.0.0.1:8000/index.html 时,它能正常工作,但现在访问 http://127.0.0.1:8000/ 会给我这个错误:

enter image description here

[编辑 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 个回答

0

在你开始搭建前端之前,把 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 文件。

0

首先要说的是!如果你想用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和静态文件的命名。它们可能会互相覆盖。

附言:谢谢你提出这么详细的问题。

1

我很久以前遇到过这个问题,后来用一个简单的方法解决了。真的很简单。 我看到你的静态文件夹不在应用的根目录,而是在错误的位置。 把它放到你的应用根目录下……因为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',
  ]

祝你好运。

1

关于 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 中,可能不是个好做法(所以叫做基础目录)。不过我不太清楚具体的来源,或者这是否真的是最佳实践。

4

错误原因

对于第一个情况

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)

撰写回答