19 · Django 引擎:MVC、路由与视图
🔗 知识图谱导航:阅读本文前,建议先掌握《18 · HTTP 与 Web 框架基石:从零手写服务器》中的 socket 请求解析与路由分发——本文把那套手写逻辑对应到 Django 的
urls.py/views.py,让你看清框架在哪里帮你省了什么。
运行环境:本文代码示例零依赖,不需要安装 Django 即可运行。Django 的路由核心逻辑用纯 Python 复现,理解后再装 Django 会事半功倍。
极客解析:Django 的"魔法"本质上是三件事:①
path()把<int:pk>转成正则;②URLResolver遍历路由表找第一个匹配;③ 视图函数接收Request返回Response。本文把这三件事拆开来跑。
Django 架构全景
┌─────────────────────────────────┐
│ 浏览器 / 客户端 │
└──────────────┬──────────────────┘
│ HTTP 请求
┌──────────────▼──────────────────┐
│ urls.py(路由层) │
│ 正则/路径匹配 → 分发到视图 │
└──────────────┬──────────────────┘
│
┌───────────────────────┼───────────────────────┐
│ │ │
┌────────────▼──────────┐ ┌──────────▼──────────┐ ┌─────────▼─────────┐
│ views.py(视图层) │ │ models.py(模型层) │ │ templates/(模板) │
│ 处理请求,返回响应 │ │ ORM 操作数据库 │ │ HTML 渲染 │
└───────────────────────┘ └─────────────────────┘ └───────────────────┘
MTV 模式对照表
┌──────────────┬──────────────────┬──────────────────────────────────┐
│ 经典 MVC │ Django MTV │ 说明 │
├──────────────┼──────────────────┼──────────────────────────────────┤
│ Model │ Model │ 数据模型,对应 models.py │
│ View │ Template │ 展示层,对应 templates/*.html │
│ Controller │ View │ 业务逻辑,对应 views.py │
│ Router │ URLconf │ 路由配置,对应 urls.py │
└──────────────┴──────────────────┴──────────────────────────────────┘
项目目录结构
mysite/
├── manage.py # 命令行工具入口
├── mysite/
│ ├── settings.py # 全局配置
│ ├── urls.py # 根路由配置
│ └── wsgi.py # WSGI 部署入口
└── tasks/ # 应用目录
├── models.py # 数据模型
├── views.py # 视图函数
├── urls.py # 应用级路由
└── tests.py # 单元测试
步步为营:核心逻辑自适应拆解
这一篇的核心是 Django 路由的三层结构:path() 把 Django 风格路径转成正则 → URLResolver 遍历路由表找匹配 → 视图函数处理请求返回响应。下面每一步都聚焦一个环节,零依赖可直接运行。
Step 1:用 @dataclass 定义简化版 Request 和 Response
痛点与机制:
Django 的 HttpRequest 和 HttpResponse 是两个大类,但核心只有几个字段。@dataclass 自动生成 __init__,field(default_factory=dict) 避免可变默认值陷阱——如果写 query: Dict = {},所有实例会共享同一个字典,这是 Python 新手最常踩的坑之一。
核心源码(逐字来自文末完整源码):
@dataclass
class Request:
method: str
path: str
query: Dict[str, str] = field(default_factory=dict)
body: Dict = field(default_factory=dict)
kwargs: Dict = field(default_factory=dict) # 路由捕获的参数
@dataclass
class Response:
status: int
body: Dict
content_type: str = "application/json"
def __str__(self) -> str:
return f"HTTP {self.status} → {self.body}"
可运行演示(补齐 Mock 数据与 print 反馈):
from dataclasses import dataclass, field
from typing import Dict
@dataclass
class Request:
method: str
path: str
# 用 default_factory=dict,而不是 query={},避免多个 Request 共用同一个字典。
query: Dict[str, str] = field(default_factory=dict)
body: Dict = field(default_factory=dict)
# kwargs 专门放路由从 URL 里抠出来的参数,比如 /tasks/42/ 里的 pk=42。
kwargs: Dict = field(default_factory=dict)
# Response 可以理解成“回信”:状态码 + 要返回给浏览器的数据。
@dataclass
class Response:
status: int
body: Dict
content_type: str = "application/json"
def __str__(self) -> str:
return f"HTTP {self.status} → {self.body}"
# 手动造一张“请求单”和一封“响应信”,先把数据结构看明白。
req = Request(method="GET", path="/tasks/42", query={"done": "false"}, kwargs={"pk": "42"})
resp = Response(status=200, body={"id": 42, "title": "学习 Django 路由"})
print("请求方法:", req.method)
print("请求路径:", req.path)
print("查询参数:", req.query)
print("路径参数:", req.kwargs)
print("响应结果:", resp)
Step 2:用 path() 把 Django 风格路径转成正则表达式
痛点与机制:
<int:pk> 是 Django 的路径参数语法,path() 把它转成正则 (?P<pk>[0-9]+)——就像把"第42号房间"翻译成"门牌号是纯数字的房间"。re.sub 用回调函数处理每个 <type:name> 占位符:int 类型转成 [0-9]+,str 类型转成 [^/]+(不含斜杠的任意字符)。(?P<name>...) 是命名捕获组,匹配后可以用 m.groupdict() 直接拿到 {"pk": "42"} 这样的字典。
核心源码(逐字来自文末完整源码):
@dataclass
class URLPattern:
pattern: str # 原始路径模式,如 "tasks/<int:pk>/"
regex: re.Pattern # 编译后的正则
view: Callable
name: str
def path(pattern: str, view: Callable, name: str = "") -> URLPattern:
"""将 Django 风格路径转换为正则: <int:pk> -> (?P<pk>[0-9]+)"""
def _convert(m: re.Match) -> str:
typ, name_ = (m.group(1), m.group(2)) if m.group(2) else ("str", m.group(1))
char_class = "[0-9]+" if typ == "int" else "[^/]+"
return f"(?P<{name_}>{char_class})"
regex_str = re.sub(r"<(?:(\w+):)?(\w+)>", _convert, pattern)
return URLPattern(pattern, re.compile(f"^{regex_str}$"), view, name or pattern)
可运行演示(补齐 Mock 数据与 print 反馈):
import re
from dataclasses import dataclass
from typing import Callable
@dataclass
class URLPattern:
pattern: str # 人写的 Django 风格路径,如 api/tasks/<int:pk>/
regex: re.Pattern # 机器真正用来匹配的正则表达式
view: Callable # 匹配成功后要调用的视图函数
name: str # 路由名字,后面 reverse() 会用到
def path(pattern: str, view: Callable, name: str = "") -> URLPattern:
"""将 Django 风格路径转换为正则: <int:pk> -> (?P<pk>[0-9]+)"""
def _convert(m: re.Match) -> str:
# m.group(1) 是类型,比如 int;m.group(2) 是参数名,比如 pk。
typ, name_ = (m.group(1), m.group(2)) if m.group(2) else ("str", m.group(1))
# int 只能匹配数字;str 匹配“不含 / 的一段文本”。
char_class = "[0-9]+" if typ == "int" else "[^/]+"
# ?P<pk> 是“命名捕获组”,匹配完可以直接得到 {'pk': '42'}。
return f"(?P<{name_}>{char_class})"
regex_str = re.sub(r"<(?:(\w+):)?(\w+)>", _convert, pattern)
return URLPattern(pattern, re.compile(f"^{regex_str}$"), view, name or pattern)
def fake_view() -> None:
pass
route = path("api/tasks/<int:pk>/", fake_view, name="task-detail")
print("原始路径:", route.pattern)
print("正则路径:", route.regex.pattern)
match = route.regex.match("api/tasks/42/")
print("匹配 api/tasks/42/ 得到:", match.groupdict() if match else None)
print("匹配 api/tasks/abc/ 得到:", route.regex.match("api/tasks/abc/"))
Step 3:用 URLResolver 实现路由匹配和 URL 反向解析
痛点与机制:
URLResolver.resolve() 就像"按图索骥":遍历路由表,对每个模式用正则匹配请求路径,找到第一个命中的就返回 (视图函数, 路径参数字典)。reverse() 是反向操作——给定路由名称和参数,还原出 URL 字符串,就像把"第42号房间"从门牌号反查出来。Django 的模板标签 url 底层就是这个逻辑,避免在模板里硬编码路径字符串。
核心源码(逐字来自文末完整源码):
class URLResolver:
def __init__(self, patterns: List[URLPattern]):
self.patterns = patterns
def resolve(self, path_str: str) -> Optional[Tuple[Callable, Dict]]:
for p in self.patterns:
m = p.regex.match(path_str)
if m:
return p.view, m.groupdict()
return None
def reverse(self, name: str, **kwargs) -> Optional[str]:
for p in self.patterns:
if p.name == name:
result = p.pattern
for k, v in kwargs.items():
result = re.sub(rf"<[^>]*:{k}>|<{k}>", str(v), result)
return result
return None
可运行演示(补齐 Mock 数据与 print 反馈):
import re
from dataclasses import dataclass
from typing import Callable, Dict, List, Optional, Tuple
@dataclass
class URLPattern:
pattern: str
regex: re.Pattern
view: Callable
name: str
def path(pattern: str, view: Callable, name: str = "") -> URLPattern:
def _convert(m: re.Match) -> str:
typ, name_ = (m.group(1), m.group(2)) if m.group(2) else ("str", m.group(1))
char_class = "[0-9]+" if typ == "int" else "[^/]+"
return f"(?P<{name_}>{char_class})"
regex_str = re.sub(r"<(?:(\w+):)?(\w+)>", _convert, pattern)
return URLPattern(pattern, re.compile(f"^{regex_str}$"), view, name or pattern)
class URLResolver:
def __init__(self, patterns: List[URLPattern]):
# patterns 就是一张“路由地图”,请求来了就按顺序找路。
self.patterns = patterns
def resolve(self, path_str: str) -> Optional[Tuple[Callable, Dict]]:
# resolve 是“从 URL 找视图”:命中后返回视图函数 + 路径参数。
for p in self.patterns:
m = p.regex.match(path_str)
if m:
return p.view, m.groupdict()
return None
def reverse(self, name: str, **kwargs) -> Optional[str]:
# reverse 是“从路由名反推 URL”:模板和重定向里非常常用。
for p in self.patterns:
if p.name == name:
result = p.pattern
for k, v in kwargs.items():
result = re.sub(rf"<[^>]*:{k}>|<{k}>", str(v), result)
return result
return None
def task_detail_view() -> None:
pass
resolver = URLResolver([path("api/tasks/<int:pk>/", task_detail_view, name="task-detail")])
resolved = resolver.resolve("api/tasks/2/")
print("resolve 找到的视图:", resolved[0].__name__ if resolved else None)
print("resolve 抠出的参数:", resolved[1] if resolved else None)
print("reverse 生成 URL:", resolver.reverse("task-detail", pk=99))
print("不存在的路由:", resolver.resolve("api/unknown/"))
Step 4:用 dispatch 把请求路由到对应视图函数
痛点与机制:
dispatch 是整条链路的"总调度员":先调 resolver.resolve() 找到视图函数和路径参数,再把 method/path/query/body/kwargs 打包成 Request 对象传给视图函数。这正是 Django BaseHandler.get_response() 的核心逻辑——框架帮你做的就是这个组装过程,视图函数只需关注业务逻辑。
核心源码(逐字来自文末完整源码):
def dispatch(method: str, path_str: str, query: Dict = None, body: Dict = None) -> Response:
result = resolver.resolve(path_str)
if result is None:
return Response(404, {"error": f"No URL pattern matches '{path_str}'"})
view, kwargs = result
req = Request(method=method, path=path_str,
query=query or {}, body=body or {}, kwargs=kwargs)
return view(req)
可运行演示(补齐 Mock 数据与 print 反馈):
import re
from dataclasses import dataclass, field
from typing import Callable, Dict, List, Optional, Tuple
@dataclass
class Request:
method: str
path: str
query: Dict[str, str] = field(default_factory=dict)
body: Dict = field(default_factory=dict)
kwargs: Dict = field(default_factory=dict)
@dataclass
class Response:
status: int
body: Dict
def __str__(self) -> str:
return f"HTTP {self.status} → {self.body}"
@dataclass
class URLPattern:
pattern: str
regex: re.Pattern
view: Callable
name: str
def path(pattern: str, view: Callable, name: str = "") -> URLPattern:
def _convert(m: re.Match) -> str:
typ, name_ = (m.group(1), m.group(2)) if m.group(2) else ("str", m.group(1))
char_class = "[0-9]+" if typ == "int" else "[^/]+"
return f"(?P<{name_}>{char_class})"
return URLPattern(pattern, re.compile("^" + re.sub(r"<(?:(\w+):)?(\w+)>", _convert, pattern) + "$"), view, name or pattern)
class URLResolver:
def __init__(self, patterns: List[URLPattern]):
self.patterns = patterns
def resolve(self, path_str: str) -> Optional[Tuple[Callable, Dict]]:
for p in self.patterns:
m = p.regex.match(path_str)
if m:
return p.view, m.groupdict()
return None
TASKS = [{"id": 1, "title": "写教程", "done": False}, {"id": 2, "title": "写测试", "done": True}]
def task_detail(request: Request) -> Response:
# dispatch 会把 URL 里的 pk 放进 request.kwargs,这里直接使用。
pk = int(request.kwargs["pk"])
for task in TASKS:
if task["id"] == pk:
return Response(200, {"task": task})
return Response(404, {"error": f"Task {pk} not found"})
resolver = URLResolver([path("api/tasks/<int:pk>/", task_detail, name="task-detail")])
def dispatch(method: str, path_str: str, query: Dict = None, body: Dict = None) -> Response:
result = resolver.resolve(path_str)
if result is None:
return Response(404, {"error": f"No URL pattern matches '{path_str}'"})
view, kwargs = result
req = Request(method=method, path=path_str,
query=query or {}, body=body or {}, kwargs=kwargs)
return view(req)
print("命中任务:", dispatch("GET", "api/tasks/2/"))
print("任务不存在:", dispatch("GET", "api/tasks/99/"))
print("路由不存在:", dispatch("GET", "api/missing/"))
Step 5:用视图函数处理路径参数和查询字符串过滤
痛点与机制:
task_list 从 request.query.get("done") 读取查询参数,task_detail 从 request.kwargs["pk"] 读取路径参数——这两个来源对应 Django 里的 request.GET.get("done") 和 kwargs["pk"]。task_detail 里的 int(request.kwargs["pk"]) 是必要的类型转换:正则捕获组返回的永远是字符串,即使路径模式是 <int:pk>,Django 也会在视图调用前帮你转换,这里手动转换是为了演示这个细节。
核心源码(逐字来自文末完整源码):
def task_list(request: Request) -> Response:
done_filter = request.query.get("done")
tasks = TASKS
if done_filter is not None:
tasks = [t for t in tasks if str(t["done"]).lower() == done_filter.lower()]
return Response(200, {"tasks": tasks, "total": len(tasks)})
def task_detail(request: Request) -> Response:
pk = int(request.kwargs["pk"])
for t in TASKS:
if t["id"] == pk:
return Response(200, {"task": t})
return Response(404, {"error": f"Task {pk} not found"})
def task_search(request: Request) -> Response:
q = request.query.get("q", "").lower()
results = [t for t in TASKS if q in t["title"].lower()]
return Response(200, {"results": results, "query": q})
def health(request: Request) -> Response:
return Response(200, {"status": "ok", "time": datetime.now().isoformat()})
可运行演示(补齐 Mock 数据与 print 反馈):
from dataclasses import dataclass, field
from datetime import datetime
from typing import Dict
@dataclass
class Request:
method: str
path: str
query: Dict[str, str] = field(default_factory=dict)
body: Dict = field(default_factory=dict)
kwargs: Dict = field(default_factory=dict)
@dataclass
class Response:
status: int
body: Dict
def __str__(self) -> str:
return f"HTTP {self.status} → {self.body}"
TASKS = [
{"id": 1, "title": "完成 Django 路由章节", "done": False},
{"id": 2, "title": "写单元测试", "done": True},
{"id": 3, "title": "部署到服务器", "done": False},
]
def task_list(request: Request) -> Response:
done_filter = request.query.get("done")
tasks = TASKS
if done_filter is not None:
# 查询字符串都是文本,所以把布尔值也转成文本再比较。
tasks = [t for t in tasks if str(t["done"]).lower() == done_filter.lower()]
return Response(200, {"tasks": tasks, "total": len(tasks)})
def task_detail(request: Request) -> Response:
# URL 捕获出来的 pk 先是字符串,真正查数据前要转成 int。
pk = int(request.kwargs["pk"])
for t in TASKS:
if t["id"] == pk:
return Response(200, {"task": t})
return Response(404, {"error": f"Task {pk} not found"})
def task_search(request: Request) -> Response:
q = request.query.get("q", "").lower()
results = [t for t in TASKS if q in t["title"].lower()]
return Response(200, {"results": results, "query": q})
def health(request: Request) -> Response:
return Response(200, {"status": "ok", "time": datetime.now().isoformat()})
print("全部任务:", task_list(Request("GET", "api/tasks/")).body["total"])
print("未完成任务:", task_list(Request("GET", "api/tasks/", query={"done": "False"})).body["total"])
print("任务#2标题:", task_detail(Request("GET", "api/tasks/2/", kwargs={"pk": "2"})).body["task"]["title"])
print("健康检查:", health(Request("GET", "health/")).body["status"])
Step 6:用 task_search 实现 ?q= 关键词搜索
痛点与机制:
task_search 用 q in t["title"].lower() 做大小写不敏感的包含匹配,对应 Django ORM 里的 title__icontains=q。request.query.get("q", "").lower() 的 .lower() 是关键——用户输入"部署"和"部署"应该得到相同结果。这个视图演示了"查询参数 → 过滤逻辑 → 返回子集"的标准模式,是搜索接口的最小实现。
核心源码(逐字来自文末完整源码):
def task_list(request: Request) -> Response:
done_filter = request.query.get("done")
tasks = TASKS
if done_filter is not None:
tasks = [t for t in tasks if str(t["done"]).lower() == done_filter.lower()]
return Response(200, {"tasks": tasks, "total": len(tasks)})
def task_detail(request: Request) -> Response:
pk = int(request.kwargs["pk"])
for t in TASKS:
if t["id"] == pk:
return Response(200, {"task": t})
return Response(404, {"error": f"Task {pk} not found"})
def task_search(request: Request) -> Response:
q = request.query.get("q", "").lower()
results = [t for t in TASKS if q in t["title"].lower()]
return Response(200, {"results": results, "query": q})
def health(request: Request) -> Response:
return Response(200, {"status": "ok", "time": datetime.now().isoformat()})
可运行演示(补齐 Mock 数据与 print 反馈):
from dataclasses import dataclass, field
from typing import Dict
@dataclass
class Request:
method: str
path: str
query: Dict[str, str] = field(default_factory=dict)
body: Dict = field(default_factory=dict)
kwargs: Dict = field(default_factory=dict)
@dataclass
class Response:
status: int
body: Dict
TASKS = [
{"id": 1, "title": "完成 Django 路由章节", "done": False},
{"id": 2, "title": "写单元测试", "done": True},
{"id": 3, "title": "部署到服务器", "done": False},
]
def task_search(request: Request) -> Response:
# q 是用户在 ?q= 后面输入的关键词;没有传就默认空字符串。
q = request.query.get("q", "").lower()
# 列表推导式像“筛子”:只留下标题中包含关键词的任务。
results = [t for t in TASKS if q in t["title"].lower()]
return Response(200, {"results": results, "query": q})
for keyword in ["部署", "单元", "不存在"]:
resp = task_search(Request("GET", "api/tasks/search/", query={"q": keyword}))
titles = [item["title"] for item in resp.body["results"]]
print(f"搜索 {keyword!r} -> {titles}")
Step 7:用 print_routes 打印路由表,看 Django 风格路径到正则的转换
痛点与机制:
print_routes 把路由表格式化成表格,让你一眼看清每条路由的名称、原始模式和编译后的正则。reverse() 演示了命名路由的价值:给路由起名字后,生成 URL 时不需要硬编码路径字符串,路径变了只需改 urlpatterns,所有用 reverse() 生成的 URL 自动更新。
核心源码(逐字来自文末完整源码):
def print_routes() -> None:
print(f"\n{'─'*65}")
print(f" {'名称':<20} {'模式':<30} {'正则'}")
print(f"{'─'*65}")
for p in urlpatterns:
print(f" {p.name:<20} {p.pattern:<30} {p.regex.pattern}")
print(f"{'─'*65}")
print("\n URL 反向解析示例:")
print(f" reverse('task-detail', pk=42) → {resolver.reverse('task-detail', pk=42)}")
print(f" reverse('task-list') → {resolver.reverse('task-list')}")
可运行演示(补齐 Mock 数据与 print 反馈):
import re
from dataclasses import dataclass
from typing import Callable, List, Optional
@dataclass
class URLPattern:
pattern: str
regex: re.Pattern
view: Callable
name: str
def path(pattern: str, view: Callable, name: str = "") -> URLPattern:
def _convert(m: re.Match) -> str:
typ, name_ = (m.group(1), m.group(2)) if m.group(2) else ("str", m.group(1))
char_class = "[0-9]+" if typ == "int" else "[^/]+"
return f"(?P<{name_}>{char_class})"
return URLPattern(pattern, re.compile("^" + re.sub(r"<(?:(\w+):)?(\w+)>", _convert, pattern) + "$"), view, name or pattern)
class URLResolver:
def __init__(self, patterns: List[URLPattern]):
self.patterns = patterns
def reverse(self, name: str, **kwargs) -> Optional[str]:
for p in self.patterns:
if p.name == name:
result = p.pattern
for k, v in kwargs.items():
result = re.sub(rf"<[^>]*:{k}>|<{k}>", str(v), result)
return result
return None
def fake_view() -> None:
pass
urlpatterns = [
path("health/", fake_view, name="health"),
path("api/tasks/", fake_view, name="task-list"),
path("api/tasks/<int:pk>/", fake_view, name="task-detail"),
]
resolver = URLResolver(urlpatterns)
def print_routes() -> None:
print(f"\n{'─'*65}")
print(f" {'名称':<20} {'模式':<30} {'正则'}")
print(f"{'─'*65}")
for p in urlpatterns:
print(f" {p.name:<20} {p.pattern:<30} {p.regex.pattern}")
print(f"{'─'*65}")
print("\n URL 反向解析示例:")
print(f" reverse('task-detail', pk=42) → {resolver.reverse('task-detail', pk=42)}")
print(f" reverse('task-list') → {resolver.reverse('task-list')}")
print_routes()
Step 8:用 main 做 demo/routes 两种模式的 CLI 总入口
痛点与机制:
main 用 argparse 做 CLI 入口:--mode demo 跑完整的请求演示,--mode routes 打印路由表。这个"演示模式分离"的设计让读者不改代码就能切换观察角度——先看路由表理解结构,再看请求演示理解行为。run_demo 里的测试用例覆盖了正常路径、查询过滤、路径参数、404 四种场景,是最小化的集成测试。
核心源码(逐字来自文末完整源码):
def main() -> None:
parser = argparse.ArgumentParser(description="Django 路由与视图演示(零依赖)")
parser.add_argument("--mode", choices=["demo", "routes"], default="demo",
help="demo=请求演示, routes=打印路由表")
args = parser.parse_args()
if args.mode == "routes":
print_routes()
else:
run_demo()
可运行演示(补齐 Mock 数据与 print 反馈):
import argparse
import sys
def print_routes() -> None:
# routes 模式:只看路由表,适合先理解结构。
print("routes 模式: 打印 urlpatterns 和 reverse 示例")
def run_demo() -> None:
# demo 模式:跑完整请求案例,适合验证行为。
print("demo 模式: 依次模拟 health、tasks、detail、search、404")
def main() -> None:
parser = argparse.ArgumentParser(description="Django 路由与视图演示(零依赖)")
parser.add_argument("--mode", choices=["demo", "routes"], default="demo",
help="demo=请求演示, routes=打印路由表")
args = parser.parse_args()
if args.mode == "routes":
print_routes()
else:
run_demo()
# 模拟用户在终端输入两条命令,不需要真的敲两次。
for mode in ["routes", "demo"]:
sys.argv = ["prog", "--mode", mode]
print(f">>> python3 19-python-django-intro.py --mode {mode}")
main()
极客实战:完整源码与运行
现在,把上面的积木拼起来,将以下完整代码放进你的编辑器,运行它。先看整体闭环,再回头逐段改参数,你会更容易建立工程直觉。
#!/usr/bin/env python3
"""
19-python-django-intro.py
不安装 Django,用纯 Python 演示 Django 路由匹配核心逻辑
用法:
python3 19-python-django-intro.py --mode demo
python3 19-python-django-intro.py --mode routes
"""
import argparse
import re
from dataclasses import dataclass, field
from typing import Callable, Dict, List, Optional, Tuple
from datetime import datetime
# ─── 简化版 Request / Response ───────────────────────────────────────────────
@dataclass
class Request:
method: str
path: str
query: Dict[str, str] = field(default_factory=dict)
body: Dict = field(default_factory=dict)
kwargs: Dict = field(default_factory=dict) # 路由捕获的参数
@dataclass
class Response:
status: int
body: Dict
content_type: str = "application/json"
def __str__(self) -> str:
return f"HTTP {self.status} → {self.body}"
# ─── 简化版 URLconf ───────────────────────────────────────────────────────────
@dataclass
class URLPattern:
pattern: str # 原始路径模式,如 "tasks/<int:pk>/"
regex: re.Pattern # 编译后的正则
view: Callable
name: str
def path(pattern: str, view: Callable, name: str = "") -> URLPattern:
"""将 Django 风格路径转换为正则: <int:pk> -> (?P<pk>[0-9]+)"""
def _convert(m: re.Match) -> str:
typ, name_ = (m.group(1), m.group(2)) if m.group(2) else ("str", m.group(1))
char_class = "[0-9]+" if typ == "int" else "[^/]+"
return f"(?P<{name_}>{char_class})"
regex_str = re.sub(r"<(?:(\w+):)?(\w+)>", _convert, pattern)
return URLPattern(pattern, re.compile(f"^{regex_str}$"), view, name or pattern)
class URLResolver:
def __init__(self, patterns: List[URLPattern]):
self.patterns = patterns
def resolve(self, path_str: str) -> Optional[Tuple[Callable, Dict]]:
for p in self.patterns:
m = p.regex.match(path_str)
if m:
return p.view, m.groupdict()
return None
def reverse(self, name: str, **kwargs) -> Optional[str]:
for p in self.patterns:
if p.name == name:
result = p.pattern
for k, v in kwargs.items():
result = re.sub(rf"<[^>]*:{k}>|<{k}>", str(v), result)
return result
return None
# ─── 模拟视图函数 ─────────────────────────────────────────────────────────────
TASKS = [
{"id": 1, "title": "完成 Django 路由章节", "done": False},
{"id": 2, "title": "写单元测试", "done": True},
{"id": 3, "title": "部署到服务器", "done": False},
]
def task_list(request: Request) -> Response:
done_filter = request.query.get("done")
tasks = TASKS
if done_filter is not None:
tasks = [t for t in tasks if str(t["done"]).lower() == done_filter.lower()]
return Response(200, {"tasks": tasks, "total": len(tasks)})
def task_detail(request: Request) -> Response:
pk = int(request.kwargs["pk"])
for t in TASKS:
if t["id"] == pk:
return Response(200, {"task": t})
return Response(404, {"error": f"Task {pk} not found"})
def task_search(request: Request) -> Response:
q = request.query.get("q", "").lower()
results = [t for t in TASKS if q in t["title"].lower()]
return Response(200, {"results": results, "query": q})
def health(request: Request) -> Response:
return Response(200, {"status": "ok", "time": datetime.now().isoformat()})
# ─── 路由表 ───────────────────────────────────────────────────────────────────
urlpatterns = [
path("health/", health, name="health"),
path("api/tasks/", task_list, name="task-list"),
path("api/tasks/<int:pk>/", task_detail, name="task-detail"),
path("api/tasks/search/", task_search, name="task-search"),
]
resolver = URLResolver(urlpatterns)
def dispatch(method: str, path_str: str, query: Dict = None, body: Dict = None) -> Response:
result = resolver.resolve(path_str)
if result is None:
return Response(404, {"error": f"No URL pattern matches '{path_str}'"})
view, kwargs = result
req = Request(method=method, path=path_str,
query=query or {}, body=body or {}, kwargs=kwargs)
return view(req)
# ─── 打印路由表 ───────────────────────────────────────────────────────────────
def print_routes() -> None:
print(f"\n{'─'*65}")
print(f" {'名称':<20} {'模式':<30} {'正则'}")
print(f"{'─'*65}")
for p in urlpatterns:
print(f" {p.name:<20} {p.pattern:<30} {p.regex.pattern}")
print(f"{'─'*65}")
print("\n URL 反向解析示例:")
print(f" reverse('task-detail', pk=42) → {resolver.reverse('task-detail', pk=42)}")
print(f" reverse('task-list') → {resolver.reverse('task-list')}")
# ─── 演示 ─────────────────────────────────────────────────────────────────────
def run_demo() -> None:
cases = [
("GET", "health/", {}, None),
("GET", "api/tasks/", {}, None),
("GET", "api/tasks/", {"done": "False"}, None),
("GET", "api/tasks/2/", {}, None),
("GET", "api/tasks/99/", {}, None),
("GET", "api/tasks/search/", {"q": "部署"}, None),
("GET", "api/nonexistent/", {}, None),
]
print(f"\n{'═'*60}")
print(" Django 路由匹配演示(零依赖)")
print(f"{'═'*60}")
for method, path_str, query, body in cases:
resp = dispatch(method, path_str, query, body)
icon = "✅" if resp.status < 400 else "❌"
print(f"\n {icon} {method} /{path_str}")
if query:
print(f" query: {query}")
print(f" → {resp}")
# ─── 入口 ─────────────────────────────────────────────────────────────────────
def main() -> None:
parser = argparse.ArgumentParser(description="Django 路由与视图演示(零依赖)")
parser.add_argument("--mode", choices=["demo", "routes"], default="demo",
help="demo=请求演示, routes=打印路由表")
args = parser.parse_args()
if args.mode == "routes":
print_routes()
else:
run_demo()
if __name__ == "__main__":
main()
$ python3 19-python-django-intro.py --mode routes
─────────────────────────────────────────────────────────────────
名称 模式 正则
─────────────────────────────────────────────────────────────────
health health/ ^health/$
task-list api/tasks/ ^api/tasks/$
task-detail api/tasks/<int:pk>/ ^api/tasks/(?P<pk>[0-9]+)/$
task-search api/tasks/search/ ^api/tasks/search/$
─────────────────────────────────────────────────────────────────
URL 反向解析示例:
reverse('task-detail', pk=42) -> api/tasks/42/
reverse('task-list') -> api/tasks/
$ python3 19-python-django-intro.py --mode demo
════════════════════════════════════════════════════════════
Django 路由匹配演示(零依赖)
════════════════════════════════════════════════════════════
✅ GET /health/
-> HTTP 200 -> {'status': 'ok', 'time': '2026-04-17T23:00:00'}
✅ GET /api/tasks/
-> HTTP 200 -> {'tasks': [...], 'total': 3}
✅ GET /api/tasks/
query: {'done': 'False'}
-> HTTP 200 -> {'tasks': [...], 'total': 2}
✅ GET /api/tasks/2/
-> HTTP 200 -> {'task': {'id': 2, 'title': '写单元测试', 'done': True}}
❌ GET /api/tasks/99/
-> HTTP 404 -> {'error': 'Task 99 not found'}
✅ GET /api/tasks/search/
query: {'q': '部署'}
-> HTTP 200 -> {'results': [{'id': 3, 'title': '部署到服务器', 'done': False}], 'query': '部署'}
❌ GET /api/nonexistent/
-> HTTP 404 -> {'error': "No URL pattern matches 'api/nonexistent/'"}
小结
| 概念 | 一句话记忆 |
|---|---|
path() |
<int:pk> 转正则 (?P<pk>[0-9]+),<str:slug> 转 [^/]+ |
URLResolver.resolve() |
遍历路由表,返回第一个匹配的 (view, kwargs) |
URLResolver.reverse() |
命名路由 + 参数 → URL 字符串,避免硬编码路径 |
dispatch() |
组装 Request,查路由表,调视图函数 |
request.query |
对应 Django 的 request.GET,查询字符串参数 |
request.kwargs |
路径参数字典,对应 Django 视图函数的 **kwargs |
include() |
大型项目把每个 App 的路由放在自己的 urls.py,根路由用 include() 组合 |
⏱ NexDo Time(5 分钟)
挑战:给路由系统加一个 <str:slug> 路径参数支持,实现 GET /api/tasks/search/<str:keyword>/ 风格的路径搜索(区别于查询字符串 ?q=)。
具体步骤:
- 在
urlpatterns里新增一条路由:path("api/tasks/by-title/<str:keyword>/", task_by_title, name="task-by-title") - 实现
task_by_title(request: Request) -> Response:从request.kwargs["keyword"]取关键词,过滤TASKS列表 - 用
dispatch("GET", "api/tasks/by-title/部署/")验证路由匹配和视图逻辑 - 用
resolver.reverse("task-by-title", keyword="部署")验证反向解析
Don’t wait for next time, do it in the next moment.