Django2:配置与路由——一个请求在 Django 里怎么走
Django 实战:做一个个人博客 · 第 2 / 8 篇(展开目录)
全系列用 Django 5.2 LTS 做一个个人博客:游客看文章,作者登录后写文章,管理员维护分类和账号。Django1—Django8 是文章序号,不是 Django 版本号。
本篇目标: 搞清一个请求在 Django 内部经过哪些环节;读懂 settings.py 和两个 urls.py。
能回答的问题: 为什么关闭 DEBUG 后网站报 400?为什么新建的应用后台看不到?为什么写了视图却是 404?
1. 一个请求的旅程
以访问 /p/first-post/ 为例:
浏览器发出 GET /p/first-post/
→ 中间件依次处理请求(会话、身份、CSRF 检查……)
→ config/urls.py:以空前缀开头,交给 blog/urls.py
→ blog/urls.py:匹配 p/<slug:slug>/,提取 slug="first-post"
→ 视图 post_detail:查询已发布的这篇文章
→ 模板 post_detail.html:把文章渲染成 HTML
→ 中间件按相反顺序处理响应
→ 浏览器显示页面
请求可能在任何一个环节提前结束。选择下面的场景,看看请求分别停在哪里:
交互演示加载中…(需要启用 JavaScript)
这也是排错顺序
先判断请求停在哪一层:400 看域名配置,403 看 CSRF,404 先看路由再看查询条件,500 看视图代码和数据库。不要一出错就怀疑“Django 没装好”。
2. 完整的 settings.py
示例项目用一个配置文件,通过环境变量区分本地和生产。本地什么都不设,就是开发模式;上线时由第 8 篇的 Compose 传入生产配置。
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"},
}
不用一次看懂全部。按用途分成五组,下面逐组讲:
| 分组 | 配置项 | 在哪一节 |
|---|---|---|
| 运行模式与安全 | DEBUG、SECRET_KEY、ALLOWED_HOSTS |
第 3 节 |
| 应用与中间件 | INSTALLED_APPS、MIDDLEWARE |
第 4、5 节 |
| 路由与模板 | ROOT_URLCONF、TEMPLATES、STATIC_* |
第 6、7 节 |
| 数据库与语言 | DATABASES、TIME_ZONE |
第 7 节;切换 PostgreSQL 见第 8 篇 |
| HTTPS 与日志 | SECURE_*、LOGGING |
第 8 篇 |
3. DEBUG、SECRET_KEY 与域名
DEBUG 打开时,出错页面会显示调用栈和配置,方便开发;生产环境必须关闭,否则会泄露内部信息。
环境变量都是字符串
bool("0") 的结果是 True。所以这里用 == "1" 判断开关,并且只认 0 和 1,写 true、yes 都不会生效。
SECRET_KEY 用于 Django 的签名(会话、CSRF 等),必须是足够长的随机值。代码里的回退值只能在本机开发用;生产环境没设置时会直接拒绝启动。它和用户密码、数据库密码是三种不同的东西。
ALLOWED_HOSTS 列出允许访问的域名或 IP,不带 https://、端口和路径。用列表以外的域名访问会得到 400。
DJANGO_ALLOWED_HOSTS=blog.example.com,127.0.0.1
DJANGO_CSRF_TRUSTED_ORIGINS=https://blog.example.com
CSRF_TRUSTED_ORIGINS 要带协议(https://),它告诉 CSRF 检查哪些来源可信。它不是关闭 CSRF,也不是跨域(CORS)配置。
4. 应用为什么要注册
startapp blog 只是生成了一个目录。只有把 "blog.apps.BlogConfig" 加进 INSTALLED_APPS,Django 才会:
- 加载
blog/models.py里的模型,并为它们生成迁移; - 在
blog/templates/、blog/static/中查找模板和静态文件。
注册之后,后台能看到模型还需要 admin.py 注册,网页能访问还需要路由连接。这三步缺一不可:
创建应用目录 → 加入 INSTALLED_APPS → 在 admin.py 注册模型 / 在 urls.py 连接视图
5. 中间件为什么讲究顺序
中间件像一层层“洋葱皮”:请求从上到下穿过 MIDDLEWARE 列表,响应从下到上返回。
| 中间件 | 作用 |
|---|---|
SecurityMiddleware |
HTTPS 跳转、安全响应头 |
SessionMiddleware |
读取会话 Cookie,提供 request.session |
CommonMiddleware |
检查 Host、规范化 URL |
CsrfViewMiddleware |
拦截没有有效 token 的 POST |
AuthenticationMiddleware |
根据会话设置 request.user |
MessageMiddleware |
支持“保存成功”这类一次性提示 |
XFrameOptionsMiddleware |
禁止别的网站用 iframe 嵌入本站 |
顺序有依赖关系:AuthenticationMiddleware 要用会话,所以必须排在 SessionMiddleware 之后。不要随意调整这个列表的顺序。
6. 路由:URL 怎样找到视图
总路由 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"),
]
三个要点:
- 路径转换器 负责提取并校验参数:
<int:pk>只接受整数,<slug:slug>只接受字母、数字、连字符、下划线。所以中文标题要配一个英文网址标识,例如“我的第一篇文章” →my-first-post。 name给路由起名。代码里用reverse("blog:detail", kwargs={"slug": ...}),模板里用{% url 'blog:list' %},以后改路径不必到处找硬编码链接。app_name = "blog"是命名空间,所以名字写成blog:list而不是list,避免和其他应用重名。
手工搭建的读者注意
上面的路由指向第 5–7 篇才写的视图。如果你走的是路线 A,现在只接入已经写好的视图,否则启动时会报 AttributeError。
7. 模板、静态文件、数据库与时区
模板: DIRS 把项目根目录的 templates/ 加入搜索路径,APP_DIRS=True 让 Django 也去各应用的 templates/ 里找。本例统一放在根目录的 templates/。
静态文件: CSS 源文件在 blog/static/blog/style.css。STATIC_URL 是浏览器访问的前缀,STATIC_ROOT 是上线时 collectstatic 汇总文件的输出目录——平时不要在那里改 CSS。
数据库: 本地默认用 SQLite(项目目录下的 db.sqlite3)。设置 DB_ENGINE=postgres 后改用 PostgreSQL,第 8 篇会用到。
时区: TIME_ZONE="Asia/Shanghai" 加 USE_TZ=True 表示数据库里存 UTC,显示时转换成北京时间。代码里取当前时间要用 django.utils.timezone.now(),不要用 datetime.now()。
8. 确认配置真的生效
临时用生产模式检查一下(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。检查完恢复开发模式:
unset DJANGO_DEBUG DJANGO_SECRET_KEY DJANGO_ALLOWED_HOSTS
不要把整个 settings 打印到日志
里面可能有数据库密码。排查时只打印需要的那一项。
小练习
1. 部署后访问 https://blog.example.com 得到 400,最可能是哪项配置的问题?
ALLOWED_HOSTS 里没有 blog.example.com。注意只写域名,不写 https://。
2. 写了视图 about,访问 /about/ 却是 404,按什么顺序排查?
blog/urls.py有没有path("about/", views.about, name="about");config/urls.py有没有include("blog.urls");- 路径末尾的斜杠是否一致。
404 而不是 500,说明请求根本没到视图——先查路由。可以在上面的演示里选“地址写错”的场景对照。
3. 设置了 DJANGO_DEBUG=true 想打开调试,为什么没生效?
本例的判断是 os.getenv("DJANGO_DEBUG", "1") == "1","true" == "1" 为假,所以 DEBUG 反而被关掉了。环境变量都是字符串,本例约定只用 0 和 1。
- 感谢你赐予我前进的力量

