4.1 视图函数与 URL 路由 本节摘要:视图函数是 Django 最小单位的"请求处理器":收一个请求对象,还一个响应对象,中间随你施展。本节讲视图的输入输出契约、URL 配置的分层组织、命名路由与反向解析,以及 shortcut 函数 family。路由是项目的门牌系统,规划好了,第 5 章模板与第 6 章表单的跳转都不会乱。 视图的契约:一个请求进,一个响应出 墨迹博客的详情页视图: 三行代码覆盖了视图的全部契约:第一,参数固定是一个 request 加路由捕获的变量;第二,必须返回响应对象;第三,查不到就抛 404——getobjector404 把"查询加不存在则 404"压缩成一行,比手写 try/except 干净得多。 返回类型也不只 HTML。
本节摘要:视图函数是 Django 最小单位的"请求处理器":收一个请求对象,还一个响应对象,中间随你施展。本节讲视图的输入输出契约、URL 配置的分层组织、命名路由与反向解析,以及 shortcut 函数 family。路由是项目的门牌系统,规划好了,第 5 章模板与第 6 章表单的跳转都不会乱。
墨迹博客的详情页视图:
from django.http import Http404 from django.shortcuts import render, get_object_or_404 from .models import Article def article_detail(request, pk): article = get_object_or_404(Article.published, pk=pk) return render(request, "blog/detail.html", {"article": article})
三行代码覆盖了视图的全部契约:第一,参数固定是一个 request 加路由捕获的变量;第二,必须返回响应对象;第三,查不到就抛 404——get_object_or_404 把"查询加不存在则 404"压缩成一行,比手写 try/except 干净得多。
返回类型也不只 HTML。返回 JSON 给前端或小程序:
from django.http import JsonResponse def article_json(request, pk): article = get_object_or_404(Article.published, pk=pk) return JsonResponse({ "title": article.title, "author": article.author.username, "created": article.created_at.isoformat(), })
重定向、文件下载、流响应,都是同一个契约的变体。shortcut 家族值得记全:render 渲染模板、redirect 跳转、get_object_or_404 与 get_list_or_404 查询兜底。它们不是语法糖,是"少写即少错"的防呆设计。
路由的组织原则是"根路由管分发,应用路由管细节":
# 根 URL 配置:只做分发 from django.urls import path, include urlpatterns = [ path("admin/", admin.site.urls), path("accounts/", include("accounts.urls")), path("", include("blog.urls")), ] # blog 应用的路由:管自己的细节 from django.urls import path from . import views app_name = "blog" # 命名空间,反向解析的前缀 urlpatterns = [ path("", views.article_list, name="article-list"), path("article/<int:pk>/", views.article_detail, name="article-detail"), path("category/<slug:slug>/", views.by_category, name="by-category"), ]
三层各有分工:根配置决定"哪些前缀归哪个应用",应用配置决定"具体路径对应哪个视图",路径转换器(int、slug、str、path)在匹配的同时完成类型校验——<int:pk> 天生不会匹配 abc,视图里的 pk 已经是整数。
每条路由的 name 是它的全局代号,配合命名空间,任何地方都能反查出真实地址:
from django.urls import reverse reverse("blog:article-detail", kwargs={"pk": 42}) # /article/42/
模板里同样用代号而非硬编码地址:
<a href="{% url 'blog:article-detail' article.pk %}">{{ article.title }}</a>
硬编码路径的代价在第 4 章之后就会显现:某天把 article 路径改成 posts,全站几十处字符串链接逐一失效;用反向解析的项目里,改动只发生在一处路由定义。这与第 2 章"唯一事实"的思想一脉相承——路径的真身只存在于路由表。
匹配按 urlpatterns 顺序自上而下,首个命中即止。这带来一条纪律:宽路径在后,窄路径在前。path("article/new/", ...) 若排在 path("article/<slug:slug>/", ...) 之后,"new" 会被当成 slug 吞掉,这种 bug 不报错、只表现为"新建页打不开",排查半天。
带参数的按分类过滤视图:
def by_category(request, slug): articles = (Article.published .filter(category__slug=slug) .select_related("author")) return render(request, "blog/list.html", {"articles": articles})
注意这里顺手用上了第 3 章的预取——分层归分层,知识会合流。

URL 常被当成纯技术问题处理,实际上它是产品面向外界的少数永久契约之一。文章详情页的地址一旦被用户收藏、被搜索引擎收录、被别的网站引用,就进入了改不起的状态——改地址等于让所有历史链接失效。所以 URL 设计要在项目早期就当成决策来做:路径分段要能体现信息层级,标识符选语义化别名而不是自增主键(可读、可传播、不泄露业务规模),避免把会变的信息固化进地址。墨迹博客早期犯过一次错:把分类名放进了文章地址,后来分类改名,几百个收录链接集体失效,补救靠的是挨个配置跳转。那次教训沉淀成一条规矩:进入 URL 的东西必须保证长期不变,会变的信息放查询参数或干脆不进地址。这类契约意识,是视图层工程素养里最容易被推迟补课的一块。
路由与视图写多了,自然会追问:视图该写多厚?健康形态是薄——收下请求、调度数据、交给模板、返回响应,四步之内结束。判断薄不薄有个朴素测试:把视图里的代码遮住框架 API,剩下的如果还能自成一段有模有样的业务叙述,说明业务逻辑渗进了视图层,该下沉到模型或独立的业务模块。墨迹博客的一次重构印证了这个方向:发文视图原本七十行,包含状态判断、配额检查、草稿合并三段逻辑,拆到模型方法与业务模块后,视图只剩十二行调度代码,而那些逻辑在测试里可以直接调用,不再需要模拟请求。视图薄下来的收益是双份的:代码更好测,分层更清楚。
函数视图直白,但列表、详情、增删改这类同质页面数量一多,重复模式显现——下一节请出类视图。