文章

19 · Django 引擎:MVC、路由与视图

#018 · 2026-04-16 · Python

🔗 知识图谱导航:阅读本文前,建议先掌握《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 的 HttpRequestHttpResponse 是两个大类,但核心只有几个字段。@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_listrequest.query.get("done") 读取查询参数,task_detailrequest.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_searchq in t["title"].lower() 做大小写不敏感的包含匹配,对应 Django ORM 里的 title__icontains=qrequest.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 总入口

痛点与机制

mainargparse 做 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=)。

具体步骤:

  1. urlpatterns 里新增一条路由:path("api/tasks/by-title/<str:keyword>/", task_by_title, name="task-by-title")
  2. 实现 task_by_title(request: Request) -> Response:从 request.kwargs["keyword"] 取关键词,过滤 TASKS 列表
  3. dispatch("GET", "api/tasks/by-title/部署/") 验证路由匹配和视图逻辑
  4. resolver.reverse("task-by-title", keyword="部署") 验证反向解析

Don’t wait for next time, do it in the next moment.