Django2:配置——看懂项目结构与请求流程
本系列目录
本系列以 Django 5.2 LTS 和个人博客为例。Django1—Django8 是文章序号。
- Django1:部署——从本地启动到服务器运行
- Django2:配置——看懂项目结构与请求流程
- Django3:数据库——模型设计、关系与迁移
- Django4:ORM——增删改查、事务与查询优化
- Django5:页面——视图、模板、搜索与分页
- Django6:使用——表单、文章管理与 Admin
- Django7:权限——登录、会话与访问控制
- Django8:维护——测试、日志、备份与升级
下载全部教程与可运行示例(ZIP)。解压后进入 django-series/example/,按第一篇运行。
上一篇把博客运行起来了。现在我们要回答三个实际问题:为什么开发时能访问、关闭 DEBUG 后却报错?为什么新建了 blog 应用,后台仍然看不到它?为什么写了视图函数,浏览器却返回 404?这些问题都需要从项目结构和配置入口开始排查。
本篇沿用第一篇的 config 项目和 blog 应用。所有路径均相对于 manage.py 所在目录。已经复制完整示例的读者可以对照阅读;手工搭建的读者应保留 Django 生成的文件,再按下面内容调整。
1. 项目与应用分别是什么
一个项目代表一个可以独立运行的网站。一个应用代表其中一组相对集中的功能。
在博客案例里,config 管理全站配置与入口,blog 管理文章、分类和标签。如果以后增加商城,可以新增 shop 应用,而不必创建第二个 Django 项目。
| 文件 | 用途 | 修改它的典型场景 |
|---|---|---|
manage.py |
加载项目后执行管理命令 | 通常保持生成内容 |
config/settings.py |
全站配置 | 接数据库、改时区、调整模板路径 |
config/urls.py |
总路由 | 把 /accounts/、/admin/ 等入口接到对应视图 |
config/wsgi.py |
WSGI 服务器入口 | Gunicorn 加载 Django 应用 |
config/asgi.py |
ASGI 服务器入口 | 将来使用 ASGI 部署时加载应用 |
blog/models.py |
数据模型 | 定义文章字段和关系 |
blog/views.py |
请求处理 | 查询文章,渲染页面或返回响应 |
blog/urls.py |
应用路由 | 将文章地址对应到视图 |
blog/forms.py |
表单字段与验证 | 验证用户提交的数据 |
WSGI 和 ASGI 是服务器与 Python 应用之间的接口。本系列使用同步视图和 Gunicorn WSGI 部署;存在 asgi.py 不表示应用已经在异步运行。Django 部署入口
2. 一次请求经过哪些步骤
以访问 /p/first-post/ 为例:
浏览器发出 GET 请求
→ 代理与应用服务器
→ 中间件处理请求
→ config/urls.py 分配路由
→ blog/urls.py 提取 slug="first-post"
→ post_detail 查询已发布文章
→ 模板接收 post 对象并生成 HTML
→ 中间件处理响应
→ 浏览器展示页面
这也给出了排错顺序。连接失败通常先看服务和端口;返回 404 先看路由和查询条件;模板不存在再查模板目录;数据库报错则查连接与迁移。不要把所有错误都归因于“Django 没装好”。
3. 完整配置文件
配套项目使用一个配置文件,通过环境变量区分本地与生产。这种组织方式适合本系列规模。将来环境增多时,可以再拆成 base.py、development.py、production.py,并通过 DJANGO_SETTINGS_MODULE 选择入口。
以下是完整 config/settings.py:
import os
from pathlib import Path
from django.core.exceptions import ImproperlyConfigured
BASE_DIR = Path(__file__).resolve().parent.parent
DEBUG = os.getenv("DJANGO_DEBUG", "1") == "1"
SECRET_KEY = os.getenv("DJANGO_SECRET_KEY", "")
if not SECRET_KEY:
if not DEBUG:
raise ImproperlyConfigured("生产环境必须设置 DJANGO_SECRET_KEY")
SECRET_KEY = "dev-only-key-for-local-learning-do-not-use-in-production"
def env_list(name, default=""):
return [item.strip() for item in os.getenv(name, default).split(",") if item.strip()]
ALLOWED_HOSTS = env_list("DJANGO_ALLOWED_HOSTS", "127.0.0.1,localhost")
CSRF_TRUSTED_ORIGINS = env_list("DJANGO_CSRF_TRUSTED_ORIGINS")
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"blog.apps.BlogConfig",
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
ROOT_URLCONF = "config.urls"
TEMPLATES = [{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [BASE_DIR / "templates"],
"APP_DIRS": True,
"OPTIONS": {"context_processors": [
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
]},
}]
WSGI_APPLICATION = "config.wsgi.application"
ASGI_APPLICATION = "config.asgi.application"
if os.getenv("DB_ENGINE", "sqlite") == "postgres":
DATABASES = {"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": os.environ["POSTGRES_DB"],
"USER": os.environ["POSTGRES_USER"],
"PASSWORD": os.environ["POSTGRES_PASSWORD"],
"HOST": os.getenv("POSTGRES_HOST", "127.0.0.1"),
"PORT": os.getenv("POSTGRES_PORT", "5432"),
"CONN_MAX_AGE": 60,
"CONN_HEALTH_CHECKS": True,
}}
else:
DATABASES = {"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
}}
AUTH_PASSWORD_VALIDATORS = [
{"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"},
{"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
]
LANGUAGE_CODE = "zh-hans"
TIME_ZONE = "Asia/Shanghai"
USE_I18N = True
USE_TZ = True
STATIC_URL = "/static/"
STATIC_ROOT = BASE_DIR / "staticfiles"
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
LOGIN_URL = "login"
LOGIN_REDIRECT_URL = "blog:mine"
LOGOUT_REDIRECT_URL = "blog:list"
# 仅在已接入可信 HTTPS 反向代理时设为 1。
HTTPS_ENABLED = os.getenv("DJANGO_HTTPS", "0") == "1"
SECURE_SSL_REDIRECT = HTTPS_ENABLED
SESSION_COOKIE_SECURE = HTTPS_ENABLED
CSRF_COOKIE_SECURE = HTTPS_ENABLED
SECURE_HSTS_SECONDS = int(os.getenv("DJANGO_HSTS_SECONDS", "0"))
if os.getenv("DJANGO_TRUST_PROXY", "0") == "1":
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"handlers": {"console": {"class": "logging.StreamHandler"}},
"root": {"handlers": ["console"], "level": "INFO"},
}
从上到下可以分成五组:目录与环境、应用与中间件、模板与路由、数据库与语言、安全与日志。下面结合本例解释。
4. DEBUG、SECRET_KEY 与域名
DEBUG=True 便于定位开发错误,但生产环境的错误页面不应展示内部配置和调用栈。本例在 Compose 中固定 DJANGO_DEBUG=0,并在生产密钥缺失时直接拒绝启动。
环境变量本身是字符串。bool("0") 仍然为真,因此本例明确使用 == "1" 解析开关。输入 true、yes 不会被本例识别为开启,文档和部署配置统一使用 0、1。
SECRET_KEY 用于 Django 的签名机制等功能,应为足够长的随机值。开发用回退值只方便本机演示。它与用户的登录密码、数据库密码是不同的凭据。
ALLOWED_HOSTS 接受主机名或 IP,不带 https://、端口和路径。例如:
DJANGO_ALLOWED_HOSTS=blog.example.com,127.0.0.1
DJANGO_CSRF_TRUSTED_ORIGINS=https://blog.example.com
这两项解决不同问题:前者验证请求的 Host,后者影响跨来源表单请求的来源信任。对于正常的同源请求,正确的 Host、协议和 CSRF token 才是基础。CSRF_TRUSTED_ORIGINS 不是关闭 CSRF,也不是 CORS 配置。Django 设置参考
5. 应用为什么要注册
python manage.py startapp blog 只生成目录。把 "blog.apps.BlogConfig" 加入 INSTALLED_APPS 后,Django 才能将它纳入应用注册表、模型加载、迁移、应用模板和静态文件查找流程。
后台能否展示模型,还取决于 admin.py 是否注册;前台能否访问业务视图,还取决于路由是否连接。这三件事不能互相替代:
创建目录 → 注册应用 → 定义并注册模型/路由
6. 中间件为什么讲究顺序
本例保留常见的默认中间件组合。SessionMiddleware 先让请求具备会话能力,AuthenticationMiddleware 随后才能根据会话确定用户。CsrfViewMiddleware 检查需要保护的请求,MessageMiddleware 支持“保存成功”等跨跳转提示。
中间件在请求方向按列表顺序进入,响应方向按相反顺序返回。因此,修改顺序可能改变行为,不能把列表当作无序开关。中间件工作机制
7. 路由是显式连接的
完整 config/urls.py:
from django.contrib import admin
from django.contrib.auth import views as auth_views
from django.urls import include, path
urlpatterns = [
path("admin/", admin.site.urls),
path("accounts/login/", auth_views.LoginView.as_view(), name="login"),
path("accounts/logout/", auth_views.LogoutView.as_view(), name="logout"),
path("", include("blog.urls")),
]
完整 blog/urls.py:
from django.urls import path
from . import views
app_name = "blog"
urlpatterns = [
path("", views.post_list, name="list"),
path("healthz/", views.health, name="health"),
path("my/posts/", views.post_mine, name="mine"),
path("posts/new/", views.post_create, name="create"),
path("posts/<int:pk>/edit/", views.post_update, name="update"),
path("posts/<int:pk>/delete/", views.post_delete, name="delete"),
path("p/<slug:slug>/", views.post_detail, name="detail"),
]
总路由把博客交给 include("blog.urls")。博客内部再将 /p/<slug>/ 分配给详情页,把 /posts/<数字>/edit/ 分配给编辑页。
<int:pk> 只接受整数,<slug:slug> 接受 ASCII 字母、数字、连字符和下划线。本例的中文文章标题与英文网址标识分开保存,例如标题“我的第一篇 Django 文章”,网址标识 my-first-django-post。
app_name = "blog" 定义命名空间,所以可以使用 reverse("blog:list") 或模板中的 {% url 'blog:list' %}。以后改变 URL 路径时,不必在页面里逐个寻找硬编码链接。URL 分发规则
以上路由指向的是完整示例里的视图。若从空项目手工搭建,尚未定义的视图会导致启动时报 AttributeError;只接入已实现的视图,或者先按第五至七篇补齐它们。
8. 模板、静态文件与时区
TEMPLATES[0]["DIRS"] 把项目根目录的 templates/ 加入搜索路径。APP_DIRS=True 同时允许查找已安装应用里的 templates/。本例采用统一模板目录,布局清晰,便于初学者寻找文件。
STATIC_URL 是浏览器访问静态文件的地址前缀,STATIC_ROOT 是 collectstatic 的输出目录。源样式位于 blog/static/blog/style.css,发布时汇集到 staticfiles/;不要把这个输出目录作为平时编辑 CSS 的地方。
TIME_ZONE="Asia/Shanghai" 配合 USE_TZ=True 使用。业务代码获取当前时间时使用 django.utils.timezone.now(),避免混用有时区与无时区的时间对象。数据库和展示层的时间表达不同,不应仅凭数据库工具里显示的小时数判断时间是否存错。时区支持
9. 如何确认配置真的生效
本地默认使用 SQLite。要临时验证生产模式,可以在 macOS/Linux 的当前终端执行:
export DJANGO_DEBUG=0
export DJANGO_SECRET_KEY="$(python -c 'import secrets; print(secrets.token_urlsafe(64))')"
export DJANGO_ALLOWED_HOSTS=127.0.0.1,localhost
python manage.py check
python manage.py shell -c 'from django.conf import settings; print(settings.DEBUG, settings.DATABASES["default"]["ENGINE"])'
预期输出包括 False 和 django.db.backends.sqlite3。本例 check 不会因此自动开启 HTTPS;是否启用 HTTPS 取决于 DJANGO_HTTPS 及代理设置。验证后恢复本地调试:
unset DJANGO_DEBUG DJANGO_SECRET_KEY DJANGO_ALLOWED_HOSTS
不要为了排查一个开关而把所有配置完整打印到共享日志,里面可能有数据库密码。只查看当前需要的字段即可。下一篇 Django3:数据库 将把博客业务具体映射到数据库表。