第6章 6.5 综合实战:MVC 框架迷你版
🎯 开场 3 分钟:为什么要学这个?
想象一下,你开了一家小餐馆。客人进门点"宫保鸡丁",服务员记下订单,交给厨房,厨房按菜谱做好,端回给客人——这个流程顺畅无比。但如果有一天,厨房突然既要知道"今天食材还剩多少",又要操心"装修风格合不合适",还要记住"哪个服务员负责哪个桌"……这厨房非炸锅不可。
MVC 框架就是餐馆的分工制度:让数据(Model)只管食材,让展示(View)只管摆盘,让协调(Controller)只管传话,各司其职,才能不乱。
上一章我们学了单例、工厂、观察者这些设计模式,这一章要把它们真正用起来——手写一个迷你 MVC 框架。学完你就能理解为什么 Laravel、Flask、Django 这些框架要这样设计,以后读别人代码不再懵。
🧱 基础 25 分钟:核心概念
什么是 MVC?用点餐系统类比
先把这三个字母拆开:
- Model(模型):数据的"仓库管理员",只管存取数据,不问数据拿来干嘛
- View(视图):数据的"形象设计\n\n
\n\n
\n\n师",只管怎么展示,不问数据从哪来 - Controller(控制器):数据的"调度员",接收请求、调用模型、选择视图
用一个生活场景理解:你打开外卖 App 点奶茶,Controller 接收"点单"请求,告诉 Model"减一杯库存",Model 改完数据返回给 Controller,Controller 再找 View"渲染订单页面"给你看。
为什么非要用 MVC?
假设不用 MVC,所有代码全塞在一个文件里:
- 某天你要改页面样式 → 得在密密麻麻的代码里找半天
- 某天你要换数据库 → 得挨个改每个用到数据库的地方
- 多人协作 → 两个人改同一个文件,改着改着就冲突了
MVC 让你改哪块就动哪块,别的地方几乎不用动。
迷你 MVC 四件套:Router + Controller + Model + View
一个最小的 MVC 框架需要四样东西:
| 组件 | 职责 | 类比 |
|---|---|---|
| Router | 根据 URL 分配请求 | 餐厅领位员 |
| Controller | 处理业务逻辑 | 传话的服务员 |
| Model | 管理数据和业务规则 | 仓库管理员 |
| View | 负责输出 HTML/JSON | 摆盘的设计师 |
第一步:写一个最简 Router
Router 的活儿很简单:看 URL,告诉框架"这个请求该哪个 Controller 处理"。
class Router:
def __init__(self):
self.routes = {}
def register(self, path, controller, method):
"""注册路由:path 是路径,controller 是控制器类,method 是处理方法名"""
self.routes[path] = (controller, method)
def dispatch(self, path, request_data):
"""分发请求:根据 path 找到对应的 controller 和 method 来处理"""
if path not in self.routes:
return {"status": 404, "body": "Not Found"}
controller_class, method_name = self.routes[path]
controller = controller_class()
method = getattr(controller, method_name)
return method(request_data)
解释一下这几行在干嘛:
- routes 是个字典,存"路径"到"处理方法"的映射
- register 负责把映射关系存进去
- dispatch 根据路径找到处理方法并调用
类比:Router 就像电话总机,有人打电话来,总机查通讯录说"这个要找王经理",就把电话转过去。
第二步:写一个基类 Controller
所有控制器都继承同一个基类,好处是统一接口,方便 Router 调用:
from abc import ABC, abstractmethod
class BaseController(ABC):
@abstractmethod
def handle(self, request):
"""处理请求,子类必须实现这个方法"""
pass
这里用到了抽象基类(上一章没讲,但很实用),意思是"我的子类必须实现 handle 方法,不然就不让你创建实例"。
第三步:写一个 Model 基类
Model 负责数据操作,我们写个基类让它更规范:
class BaseModel:
def __init__(self):
self.data = {}
def get(self, key, default=None):
"""读取数据,没有就返回默认值"""
return self.data.get(key, default)
def set(self, key, value):
"""存储数据"""
self.data[key] = value
这个 BaseModel 现在只是个内存存储,后面换成数据库就改这一层就行。
第四步:写一个 View 基类
View 负责把数据变成人能看的内容:
class BaseView:
def render(self, data):
"""把数据渲染成字符串,默认转 JSON"""
return str(data)
🔥 实战 35 分钟:3 个递进的小项目
项目 1(5 分钟):跑通"Hello World"级 MVC
先写一个最小可运行的例子,感受一下流程:
# mini_mvc.py
class Router:
def __init__(self):
self.routes = {}
def register(self, path, controller, method):
self.routes[path] = (controller, method)
def dispatch(self, path, request_data):
if path not in self.routes:
return {"status": 404, "body": "Not Found"}
controller_class, method_name = self.routes[path]
controller = controller_class()
method = getattr(controller, method_name)
return method(request_data)
class BaseController:
def handle(self, request):
pass
class HomeController(BaseController):
def handle(self, request):
return {"status": 200, "body": "欢迎来到首页!"}
class UserController(BaseController):
def handle(self, request):
return {"status": 200, "body": f"用户 {request.get('name', '匿名')} 你好!"}
# 创建路由并注册
router = Router()
router.register("/", HomeController, "handle")
router.register("/user", UserController, "handle")
# 模拟请求
if __name__ == "__main__":
print(router.dispatch("/", {}))
print(router.dispatch("/user", {"name": "小明"}))
预期输出:
{'status': 200, 'body': '欢迎来到首页!'}
{'status': 200, 'body': '用户 小明 你好!'}
这段代码演示了:请求进来 → Router 匹配路径 → 分发到对应 Controller → 返回响应。
项目 2(15 分钟):加一个"待办清单"功能
现在加点真实场景:做一个待办清单,能添加任务、查看任务列表。
# todo_app.py
class Router:
def __init__(self):
self.routes = {}
def register(self, path, controller, method):
self.routes[path] = (controller, method)
def dispatch(self, path, request_data):
if path not in self.routes:
return {"status": 404, "body": "Not Found"}
controller_class, method_name = self.routes[path]
controller = controller_class()
method = getattr(controller, method_name)
return method(request_data)
class BaseController:
pass
class TaskModel:
"""数据模型:负责存取任务数据"""
def __init__(self):
self.tasks = []
def add_task(self, title):
task_id = len(self.tasks) + 1
self.tasks.append({"id": task_id, "title": title, "done": False})
return task_id
def list_tasks(self):
return self.tasks
def complete_task(self, task_id):
for task in self.tasks:
if task["id"] == task_id:
task["done"] = True
return True
return False
class TaskView:
"""视图:负责把任务渲染成 HTML"""
def render_list(self, tasks):
html = "<h1>我的待办清单</h1><ul>"
for task in tasks:
status = "✓" if task["done"] else "○"
html += f"<li>{status} {task['title']}</li>"
html += "</ul>"
return html
def render_success(self, message):
return f"<p style='color:green'>{message}</p>"
class TaskController(BaseController):
"""控制器:协调 Model 和 View"""
def __init__(self):
self.model = TaskModel()
self.view = TaskView()
def add(self, request):
title = request.get("title", "")
if not title:
return {"status": 400, "body": "任务标题不能为空"}
self.model.add_task(title)
return {"status": 200, "body": self.view.render_success(f"已添加:{title}")}
def list(self, request):
tasks = self.model.list_tasks()
return {"status": 200, "body": self.view.render_list(tasks)}
def complete(self, request):
task_id = request.get("id")
if self.model.complete_task(int(task_id)):
return {"status": 200, "body": self.view.render_success(f"任务 {task_id} 已完成")}
return {"status": 404, "body": "任务不存在"}
# 初始化应用
router = Router()
task_controller = TaskController()
router.register("/tasks/add", type('AddController', (), {"handle": lambda r: task_controller.add(r)}), "handle")
router.register("/tasks/list", type('ListController', (), {"handle": lambda r: task_controller.list(r)}), "handle")
router.register("/tasks/complete", type('CompleteController', (), {"handle": lambda r: task_controller.complete(r)}), "handle")
# 模拟使用
if __name__ == "__main__":
# 添加任务
router.dispatch("/tasks/add", {"title": "写完 MVC 教程"})
router.dispatch("/tasks/add", {"title": "录制视频"})
# 查看列表
response = router.dispatch("/tasks/list", {})
print(response["body"])
# 完成任务
router.dispatch("/tasks/complete", {"id": 1})
# 再次查看
response = router.dispatch("/tasks/list", {})
print(response["body"])
预期输出:
<h1>我的待办清单</h1><ul><li>○ 写完 MVC 教程</li><li>○ 录制视频</li></ul>
<h1>我的待办清单</h1><ul><li>✓ 写完 MVC 教程</li><li>○ 录制视频</li></ul>
看!数据层(Model)只管存任务,视图层(View)只管生成 HTML,控制器(Controller)负责串联它们。哪天你要把 HTML 换成 JSON,只需改 View,Model 和 Controller 完全不用动。
项目 3(15 分钟):做一个"学生成绩统计"工具
组合前两个项目的能力,从 JSON 读取学生成绩,算统计指标,输出报表。
# student_stats.py
import json
class Router:
def __init__(self):
self.routes = {}
def register(self, path, controller, method):
self.routes[path] = (controller, method)
def dispatch(self, path, request_data):
if path not in self.routes:
return {"status": 404, "body": "Not Found"}
controller_class, method_name = self.routes[path]
controller = controller_class()
method = getattr(controller, method_name)
return method(request_data)
class StudentModel:
"""数据模型:学生成绩管理"""
def __init__(self):
self.students = []
def load_from_json(self, json_str):
"""从 JSON 字符串加载数据"""
self.students = json.loads(json_str)
def get_all(self):
return self.students
def average_score(self):
if not self.students:
return 0
total = sum(s["score"] for s in self.students)
return total / len(self.students)
def top_student(self):
if not self.students:
return None
return max(self.students, key=lambda s: s["score"])
class StatsView:
"""视图:输出统计报表"""
def render_report(self, stats):
report = f"""=== 学生成绩统计报表 ===
平均分:{stats['average']:.1f}
最高分:{stats['max']}
最低分:{stats['min']}
及格人数:{stats['pass_count']}/{stats['total']}
最优学生:{stats['top_student']['name']} ({stats['top_student']['score']}分)
"""
return report
def render_table(self, students):
lines = ["姓名 | 分数 | 等级"]
lines.append("-" * 25)
for s in students:
grade = "A" if s["score"] >= 90 else "B" if s["score"] >= 75 else "C" if s["score"] >= 60 else "D"
lines.append(f"{s['name']:8s}| {s['score']:5d} | {grade}")
return "\n".join(lines)
class StatsController:
"""控制器:生成统计数据"""
def __init__(self):
self.model = StudentModel()
self.view = StatsView()
def load_data(self, json_str):
self.model.load_from_json(json_str)
return {"status": 200, "body": "数据加载成功"}
def report(self, request):
students = self.model.get_all()
if not students:
return {"status": 400, "body": "没有学生数据,请先加载"}
scores = [s["score"] for s in students]
stats = {
"average": self.model.average_score(),
"max": max(scores),
"min": min(scores),
"pass_count": len([s for s in students if s["score"] >= 60]),
"total": len(students),
"top_student": self.model.top_student()
}
return {"status": 200, "body": self.view.render_report(stats)}
def table(self, request):
students = self.model.get_all()
return {"status": 200, "body": self.view.render_table(students)}
# 测试数据
test_json = json.dumps([
{"name": "张三", "score": 85},
{"name": "李四", "score": 92},
{"name": "王五", "score": 67},
{"name": "赵六", "score": 78},
{"name": "钱七", "score": 55}
])
# 启动应用
router = Router()
controller = StatsController()
router.register("/stats/load", type('LoadCtrl', (), {"handle": lambda r: controller.load_data(r)}), "handle")
router.register("/stats/report", type('ReportCtrl', (), {"handle": lambda r: controller.report(r)}), "handle")
router.register("/stats/table", type('TableCtrl', (), {"handle": lambda r: controller.table(r)}), "handle")
if __name__ == "__main__":
router.dispatch("/stats/load", test_json)
print(router.dispatch("/stats/report", {})["body"])
print(router.dispatch("/stats/table", {})["body"])
预期输出:
=== 学生成绩统计报表 ===
平均分:75.4
最高分:92
最低分:55
及格人数:4/5
最优学生:李四 (92分)
姓名 | 分数 | 等级
-------------------------
张三 | 85 | B
李四 | 92 | A
王五 | 67 | C
赵六 | 78 | B
钱七 | 55 | D
这个项目把 MVC 的优势体现得更明显了:哪天你要从 CSV 文件读数据,只需改 StudentModel.load_from_csv(),View 和 Controller 纹丝不动。
💪 进阶 20 分钟:常见坑 + 性能小贴士
坑 1:Controller 塞了太多逻辑
❌ 错误做法:
class BadController:
def handle(self, request):
# 业务逻辑全塞这儿了
data = request["data"]
result = []
for item in data:
if item["type"] == "A":
item["discount"] = 0.9
elif item["type"] == "B":
item["discount"] = 0.8
# 100 行后……
return {"status": 200, "body": result}
✅ 正确做法:业务逻辑移到 Model
class OrderModel:
def apply_discount(self, items):
for item in items:
if item["type"] == "A":
item["discount"] = 0.9
# 只写关键逻辑
return items
class GoodController:
def __init__(self):
self.model = OrderModel()
def handle(self, request):
items = self.model.apply_discount(request["data"])
return {"status": 200, "body": items}
坑来了:Controller 的活儿是"调度",不是"干活"。如果你的 Controller 超过 20 行,八成要拆分。
坑 2:Model 直接返回数据库裸数据
❌ 错误:
class BadModel:
def get_user(self, id):
return db.query("SELECT * FROM users WHERE id = ?", id) # 裸数据,格式不确定
✅ 正确:统一返回格式
class GoodModel:
def get_user(self, id):
row = db.query("SELECT * FROM users WHERE id = ?", id)
if not row:
return None
return {"id": row[0], "name": row[1], "email": row[2]} # 固定格式
坑 3:路由匹配用字符串硬编码
❌ 错误:
if path == "/user/add":
# 处理
elif path == "/user/delete":
# 处理
✅ 正确:用字典或装饰器
class Router:
def __init__(self):
self.routes = {}
def register(self, path, handler):
self.routes[path] = handler
坑 4:View 返回字符串还拼接 SQL
❌ 这个坑太常见了,说白了就是别在 View 层干数据库的活儿。View 只管渲染,数据格式在 Model 层处理好再给 View。
调试技巧:用日志而不是 print
import logging
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
class TaskController:
def add(self, request):
logger.debug(f"收到添加任务请求: {request}")
# 业务逻辑
logger.info(f"任务添加成功: {title}")
正式项目里,logging 比 print 强在哪?它有级别(DEBUG/INFO/WARNING/ERROR)、能写文件、能关掉。养成用 logger 的习惯,以后接大项目不慌。
✏️ 练习题 + 作业题
练习题(5 道,10 分钟内完成)
练习 1(2 分钟):添加一个新路由
- 输入:在项目 1 基础上,新增 /about 路由,返回"这是关于页面"
- 预期输出:{'status': 200, 'body': '这是关于页面'}
- 提示:router.register("/about", ...) 只需要改这一行
练习 2(2 分钟):给任务加"删除"功能
- 输入:在项目 2 基础上,给 TaskModel 加 delete_task 方法
- 预期输出:删除后列表里没有这个任务了
- 提示:删除可以用 list.remove() 或者列表推导式过滤
练习 3(3 分钟):换个数据源
- 输入:把项目 3 的 load_from_json 改成接收 Python 字典列表(不用 JSON 字符串)
- 预期输出:功能一样,但传入参数变了
- 提示:直接在 Model 里面 self.students = data 就行
练习 4(3 分钟):串联两个小项目
- 输入:把项目 2 的待办清单和项目 3 的统计功能合并,要求任务完成后能统计
- 预期输出:完成任务后,任务从"待办"移到"已完成"列表
- 提示:给 TaskModel 加一个 completed_tasks 列表
练习 5(挑战题,5 分钟):读懂报错
- 输入:以下代码运行后报什么错?
router = Router()
router.register("/test", None, "handle") # 注意第二个参数是 None
print(router.dispatch("/test", {}))
- 预期输出:解释为什么会报错
- 提示:
None不是类,没有handle方法
作业题(30 分钟-2 小时)
作业:做一个「迷你博客系统」
- 需求描述:实现一个极简博客,支持发文章、列表页、详情页
- 功能点:
1.POST /articles新建文章(标题 + 内容)
2.GET /articles查看文章列表(显示标题和发布时间)
3.GET /articles/<id>查看单篇文章详情 - 加分项:
1. 加一个"文章点击量"统计
2. 用单例模式确保 Model 全局唯一 - 验收标准:能跑起来 + 3 个路由都能正常返回 + 代码有注释
- 提交方式:评论区贴代码或 GitHub 链接
📚 总结 + 资源
一句话总结:MVC 把数据(Model)、展示(View)、调度(Controller)分开,让你"改哪块动哪块",代码好维护、好扩展。
3 个核心点:
- Router 负责"看到请求 → 找对 Controller"
- Model 负责"数据存取和业务逻辑"
- View 负责"把数据变成人能看的内容"
延伸学习资源:
- Flask 官方文档:https://flask.palletsprojects.com/ (真正的 MVC 框架,感受一下工业级代码长啥样)
- 《Python Web 开发实战》:董伟明著,实战派 Web 开发入门书
- Django 官方教程:https://docs.djangoproject.com/zh-hans/ (更完整的 MVC 实现,但比 Flask 重)
互动钩子:学完这章,下次打开抖音/微博,想想它们后台是不是也在用 MVC 类似的架构?在评论区聊聊你的理解,老粉优先回复!
下一章我们要进入第 7 章,会讲到 HTTP 协议与 header——搞懂这个,你就能理解"浏览器和服务器是怎么对话的",为后面写真正的 Web 应用打基础。敬请期待!

评论(0)