拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Django基础入门:从MTV架构到ORM实战搭建Web应用

Django基础入门:从MTV架构到ORM实战搭建Web应用

第一次听说“Django”,是在一次要给团队内部做小工具的时候。需求很简单:一个网页,让同事提交问题,管理员能看到列表并标记处理状态。当时我的 Python 刚学完基础语法,在 Flask 和 FastAPI 之间来回摇摆,最后选了 Django,理由非常朴素——我想要一个自带后台、自带用户认证、自带数据库迁移的系统,而不是从零去拼一堆第三方库。这篇是“Django 基础入门教程”的第一篇,目标读者是刚学完 Python、想往 Web 开发走的新手,也适合在其他框架里绕了一圈、想换个更“成套”的框架体验一下的朋友。我会拿一个很小但完整的待办事项应用做主线,从环境准备一路讲到模型、视图、模板、查询和删除,每一步的“为什么这么做”也会交代清楚。你不需要任何 Django 基础,只要装好 Python,再带一点耐心就够了。

1. Django 项目是怎么运转的:先看懂 MTV 骨架

1.1 从一次内部小需求说起

当时的需求是要处理“同事报障”这个小流程,数据模型其实非常浅:一条记录、一个状态、一个提交时间。但真正写起来才发现,报障系统它需要的功能一点都不少——后台要能方便地增删改查、前端要能展示列表、用户提交后最好还能有权限控制。

我先试了 Flask。Flask 确实轻巧,一个文件就能跑起来,但一旦出现用户登录、后台管理、数据库迁移这些需求,就得自己安装 Flask-Login、Flask-Admin、Alembic,还要操心它们之间版本兼容的问题。后来试了 FastAPI,异步性能确实好,写 API 也舒服,但当时团队需要的并不是纯 API,而是一个有页面的管理系统,FastAPI 在模板渲染和传统后台管理这块又得自己拼。

Django 在这类“内部管理系统”场景里几乎是为所欲为。它自带 Admin 后台、ORM、表单处理、认证系统,迁移功能也是一等公民。对比一下你就明白:

特性DjangoFlaskFastAPI
管理后台自带 Admin需要 Flask-Admin没有官方方案
ORM 与迁移自带并深度集成SQLAlchemy + Alembic 组合SQLAlchemy / Tortoise
用户认证自带 User 模型和权限体系第三方扩展第三方扩展
学习曲线偏陡但体系完整平坦但容易“自建轮子”API 友好但场景偏窄

所以我的第一个建议是:如果目标明确是“做一个带页面的业务系统”,Django 会帮你省掉大量重复劳动。这也是这一篇系列里反复强调的核心判断标准——先看场景,再选框架。

1.2 Django 的请求处理流程:从浏览器到页面

我第一次看 Django 文档时,被 MVT 这个概念绕了一下。后来我用一个“点餐”的例子给自己讲明白了。

用户访问一个 URL,相当于你走进餐厅坐下。餐厅里有个服务员负责看你写了什么菜,这个角色在 Django 里叫 URLconf,也就是urls.py。她看清楚之后,会把订单转给后厨,后厨就是视图(View)。视图拿到订单后,会去仓库拿食材——食材就是数据库里的数据,拿食材的工具是模型(Model)。东西备齐了,后厨开始装盘,装盘的过程是模板渲染(Template)。最后服务员端着菜到顾客面前,这就是 HttpResponse 响应。

对应下来,就是下面这条链:

浏览器请求 -> urls.py 匹配路由 -> views.py 处理逻辑 -> models.py 操作数据库 -> templates/*.html 渲染页面 -> 返回 HttpResponse

Django 文档喜欢把这种结构叫 MVT:Model、View、Template。它本质上和我们常说的 MVC(Model、View、Controller)是一回事,只是 Django 把 Controller 的角色拆给了 URL 配置加视图函数。理解这个流程比背概念重要得多,因为后面你不管写什么功能,都是在这条链上加东西。

还有一件事我特别想跟新手说清楚:当你新建一个 Django 项目,运行python manage.py runserver之后,其实已经在本地跑了一个完整的 Web 服务了。它不是静态页面预览,而是真的服务器程序。理解到这一点,你再去看后面所有操作,心里会踏实很多。

2. 环境准备:把 Python 和 Django 一次装对

2.1 版本选择:别走在 Django 支持路线图前面

入门遇到的最初一坑,不是写代码,而是版本。很多新手一上来就装 Python 最新版,然后发现某个第三方库还不支持,报错报得怀疑人生。

我目前的建议分两种情况。如果你用的是 Python 3.10 或 3.11,直接装 Django 5.0 或 5.1 都没问题,5.x 是当前主流版本,官方支持力度好。如果你的服务器环境比较保守,那就选 Django 4.2 LTS,它是长期支持版本,维护时间很长,适合部署到生产环境。Python 3.12 也可以用,但先确认你打算用的其他库是否跟上,再决定主线版本。

这里有一个判断技巧:打开终端,输入python --version先确认自己的解释器版本,再去 Django 官网看支持的 Python 版本范围。不要盲目追求“最新”,稳定组合才是关键。我自己在本地用的组合是 Python 3.11 + Django 5.0,跑下来的体验很稳。如果你用的 Python 比较老,比如 3.8 以下,那第一件事是先升级 Python 本身,而不是跟版本报错较劲。

2.2 虚拟环境:给每个项目一个独立小房间

很多新手最开始会直接在系统环境里pip install django,装完就开干。短期内没问题,但你多写几个项目后会痛苦不堪:项目 A 要 Django 4.2,项目 B 要 Django 5.0,全装在一个环境里,要么互相打架,要么升级一个就弄坏另一个。

虚拟环境就是用来解决这个问题的。它相当于给每个项目准备了一间独立的小房间,房间里的 Python 版本、第三方库都跟外面隔离。你在这个房间里怎么折腾都不会影响别的项目。

Python 自带的venv模块已经是完全够用的方案。用下面的命令创建并激活:

# 创建虚拟环境(在当前目录下生成 .venv 文件夹) python -m venv .venv # 激活虚拟环境 # Windows: .venv\Scripts\activate # macOS / Linux: source .venv/bin/activate

激活之后,你会发现命令行前面多了个(.venv)前缀,这就说明你已经在虚拟环境里了。之后所有pip install都会装进这个房间。

如果你喜欢更快的工具,可以试试uv。它是个用 Rust 写的 Python 包管理器,创建虚拟环境和安装依赖都快到离谱。我的常用组合是:

uv venv uv pip install django

uv会自动创建一个.venv文件夹,你用source .venv/bin/activate激活,后面的操作和传统方式一模一样。第一次用的时候你会被它的速度惊到,反正我现在本地写教程都用 uv 了。

2.3 安装 Django 并验证

进入虚拟环境后,安装 Django 只是两行命令的事:

pip install "Django>=5.0,<5.2"

这里指定版本范围而不是直接pip install django,是为了避免将来某天pip自作主张装了一个刚发布的新大版本,跟你的项目代码出现兼容性问题。锁一个合理范围,既保证能获得小版本更新,又不会突然跨大版本。

安装完之后,一定要验证一下安装结果:

python -c "import django; print(django.get_version())"

如果你看到类似5.0.6的输出,说明 Django 已经安装成功。还可以再执行一下django-admin --version,这个命令是后面创建项目要用的工具。如果提示“命令不存在”,多半是虚拟环境没激活,或者激活后django-admin所在的 Scripts 目录不在 PATH 里,重新激活环境试试。

到这里,环境准备就结束了。整体上不复杂,但相信我,这一步值得认真做,后面所有操作都会顺畅很多。

3. 创建项目和应用:理解 Django 的分层

3.1 startproject 之后,目录里都有什么

激活虚拟环境、装好 Django 之后,下一步就是创建项目。打开终端,进入你想放代码的目录,执行:

django-admin startproject todo_project cd todo_project

用tree或者文件管理器看一下目录结构,你会发现生成了这样一堆东西:

todo_project/ manage.py todo_project/ __init__.py settings.py urls.py asgi.py wsgi.py

新手看到嵌套的项目名大概率会愣一下,我当时也很懵:为什么外面一个todo_project,里面还有一个todo_project?

解释一下。最外层的todo_project/是整个工程的根目录,里面那个同名子目录是“项目配置目录”。真正的全局配置都放在内层:settings.py存所有配置项,urls.py管全局路由入口,wsgi.py和asgi.py是部署时给服务器用的入口文件。manage.py则是你日常开发中最常用的命令工具,启动服务、创建应用、执行迁移,全都要靠它。

一个项目下面可以挂很多应用模块,这种结构设计跟公司类似:总公司定制度、管预算,下面的各个部门各干各的业务。项目就是公司,应用就是部门。

3.2 创建 app 并注册:为什么必须加这一行

项目建好后,我们来建“部门”。一个待办事项功能,在 Django 里就是单独的一个 app。执行:

python manage.py startapp todo

这一步会生成一个todo/文件夹,里面有models.py、views.py、admin.py、migrations/等文件。你可能迫不及待想写代码了,但注意,这时候 Django 还不知道这个 app 存在。你得先去settings.py里的INSTALLED_APPS列表中登记:

INSTALLED_APPS = [ "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", "todo", # 这里 ]

刚开始我也觉得这一步很啰嗦:“我明明已经创建了 app,为什么还要手动注册?”后来才明白,INSTALLED_APPS就像公司的花名册,只有被登记在册的部门,框架才会去加载它的模型、管理后台、模板等资源。如果不加这一行,后面执行迁移时,Django 根本不会理会这个 app 里的模型。

3.3 定义第一个模型:迁移到底在干什么

Django 的模型(Model)是用 Python 类来描述的,每一个类对应数据库中的一张表,类属性对应表的字段。现在我们来写待办事项的模型。打开todo/models.py,输入:

from django.db import models class Todo(models.Model): title = models.CharField("标题", max_length=200) description = models.TextField("描述", blank=True) done = models.BooleanField("是否完成", default=False) created_at = models.DateTimeField("创建时间", auto_now_add=True) def __str__(self): return self.title

这里有几个类型要注意。CharField必须指定max_length,这是数据库层面的限制,不填会报错。DateTimeField的auto_now_add=True表示创建记录时自动写入当前时间,你不需要手动赋值,这个设计在几乎所有业务表里都会用到。

写完之后,就要把模型变成数据库表,用到两条命令:

python manage.py makemigrations python manage.py migrate

这两条命令的区别,我用做饭来打比方:makemigrations是“备菜”——根据模型的变化生成一份迁移记录文件,它不改动数据库;migrate是“下锅”——真正执行迁移记录,把表建出来。第一次执行migrate时,Django 会顺便把自带的用户、会话、权限等系统表也建好。

有个排查技巧很有用:想查看某次迁移到底会执行什么 SQL,可以运行python manage.py sqlmigrate todo 0001。这个命令会打印出 Django 即将执行的 SQL 语句。新手第一次看这个输出会特别有收获,你能直观看到CharField变成了varchar,BooleanField变成了bool,前后端的数据类型映射一下子就不神秘了。

3.4 把模型注册进 Admin 后台

Django 自带的 Admin 后台是这个框架的王牌功能,很多企业用 Django 就是冲着它去的。把模型注册进去,你就能得到一个完整的可视化增删改查界面。打开todo/admin.py:

from django.contrib import admin from .models import Todo @admin.register(Todo) class TodoAdmin(admin.ModelAdmin): list_display = ["title", "done", "created_at"] list_filter = ["done"] search_fields = ["title"]

这些是什么意思呢?list_display决定列表页展示哪些列;list_filter在侧边栏生成一个按“是否完成”筛选的按钮;search_fields给标题加一个搜索框。这三行代码看起来不起眼,实际上帮你把一套后台管理界面直接做完了。

启动服务看看效果:

python manage.py runserver

浏览器打开http://127.0.0.1:8000/admin/,登录账号是createsuperuser创建的。如果你还没创建管理员,先在终端跑一下python manage.py createsuperuser,按提示输入用户名、邮箱、密码。登录进去后,你会看到一个能增删改查 Todo 的管理界面。

到这里,第一个完整的“项目骨架 + 模型 + 后台”已经跑通了。很多人走到这一步会特别兴奋,因为“有界面了”。但我建议你先别急着继续,把模型和迁移的机制再理一遍,后面写业务代码时你会受益很多。

4. 让数据跑到页面上:View、URL、Template 三件套

4.1 一次请求的完整旅程

在 Django 里,让一个页面显示数据,永远是同一个套路:配置 URL、写视图、渲染模板。你掌握的其实是整个框架最核心的调用链。

假设用户访问/todos/这个地址。Django 会按下面的流程处理:

  1. 请求进来后,先到项目根路由todo_project/urls.py。
  2. 根路由通过include()把请求转交给todo/urls.py(如果我们配置了 app 级路由)。
  3. todo/urls.py里的path("todos/", ...)匹配到对应的视图函数。
  4. 视图函数执行逻辑,比如从数据库读取 Todo 列表。
  5. 视图把数据塞进模板上下文,调用render()生成 HTML。
  6. 返回 HttpResponse 给浏览器。

这条链路我建议你默写到条件反射的程度。因为不管是后面写登录注册、写 API 接口,还是写一个小工具站,本质都是在这条链上做替换。

4.2 写一个列表视图:函数视图先讲清楚

打开todo/views.py,我们来写一个最简单的列表视图:

from django.shortcuts import render from .models import Todo def todo_list(request): todos = Todo.objects.all().order_by("-created_at") return render(request, "todo/todo_list.html", {"todos": todos})

Todo.objects.all()拿到所有记录,order_by("-created_at")按创建时间倒序排列,负号就是 DESC,不加负号是 ASC。render()的第三个参数是 Python 字典,这个字典就是模板里能用的环境变量。

这个函数视图的逻辑很清楚:拿数据、交数据。你可能会看到网上很多人推荐直接用 Django 内置的类视图ListView,写起来更省事:

from django.views.generic import ListView from .models import Todo class TodoListView(ListView): model = Todo template_name = "todo/todo_list.html"

类视图确实代码更少,但我主张新手先写函数视图。原因很简单:类视图把很多行为封装在“魔法方法”里,你能很快做出页面,却不知道数据到底是从哪个方法来的。一旦需要自定义行为,比如过滤当前用户的数据,你会一头雾水。等函数视图写熟了,再切到类视图会非常轻松,因为底层概念是通的。

4.3 用模板把数据渲染出来

视图返回的render(request, "todo/todo_list.html", ...)会去找模板文件。为了不让模板乱糟糟,我建议在todo/应用目录下建立templates/todo/这样的层级目录,注意templates下面还要再放一层以应用名命名的目录。这样做是为了避免多个应用之间模板文件名冲突。

创建todo/templates/todo/base.html:

<!doctype html> <html> <head> <meta charset="utf-8"> <title>{% block title %}待办事项{% endblock %}</title> </head> <body> <main> {% block content %}{% endblock %} </main> </body> </html>

再创建todo/templates/todo/todo_list.html:

{% extends "todo/base.html" %} {% block title %}待办列表{% endblock %} {% block content %} <h1>待办事项</h1> <ul> {% for todo in todos %} <li> <span>{{ todo.title }}</span> {% if todo.done %} <span>已完成</span> {% else %} <span>未完成</span> {% endif %} </li> {% else %} <li>暂无待办事项</li> {% endfor %} </ul> {% endblock %}

模板语法入门只需要抓住两点:{{ variable }}用来输出变量,{% tag %}用来写逻辑,比如for、if、extends、block。{% extends "todo/base.html" %}表示这个页面继承base.html,然后通过{% block content %}填充自己的内容。模板继承的意义非常大,否则你每个页面都要复制一遍 HTML 骨架,改导航栏时就要改几十个文件,那场面很痛苦。

注意for ... else的写法。这个else是 Django 模板的特色,它表示当迭代的对象为空时执行的内容。不需要写if todos去判断,非常省事。

4.4 URL 路由:先学会path,别急着上正则

视图和模板写好了,还差最后一环——URL 映射。Django 的分层设计里,每个 app 都可以有自己的urls.py,然后通过根路由挂载。

先新建todo/urls.py:

from django.urls import path from . import views urlpatterns = [ path("todos/", views.todo_list, name="todo_list"), ]

然后在项目根路由todo_project/urls.py里挂载:

from django.contrib import admin from django.urls import path, include urlpatterns = [ path("admin/", admin.site.urls), path("", include("todo.urls")), ]

这里path("todos/", views.todo_list, name="todo_list")的name参数值得养成习惯。有了它,你在模板里可以直接用{% url 'todo_list' %}来生成 URL,而不是手写/todos/。以后路由地址改了,模板会自动跟着变,不用全局替换。

path()里还能用转换器,比如path("todo/<int:pk>/delete/", ...)中的<int:pk>会匹配数字并转成整数传给视图。对于新手,先掌握int和str两个转换器就够用 80% 的场景了。正则表达式re_path()虽然功能更强大,但上手成本高,等你在实际项目里真正遇到复杂匹配需求时再学也不迟。

5. 查询与删除:ORM 里最常见的操作姿势和坑

5.1 QuerySet 是惰性的:像“点外卖”,下单不等于开做

Django ORM 最核心的一个概念就是 QuerySet。新手最容易迷惑的是:为什么我明明写了查询,数据库却没有执行?

Todo.objects.all()返回的是一个 QuerySet 对象,它并不会立刻去数据库拿数据。这个设计是故意的,叫“惰性求值”。它的价值在于链式操作:你可以在all()之后继续挂filter()、exclude()、order_by(),这些操作追加的是查询条件,而不是真的查了多次数据库。

QuerySet 什么时候真正执行查询呢?大概在这些时刻:

  • 第一次遍历它(for todo in todos)
  • 调用list()把它转成列表
  • 调用len()求长度
  • 调用bool()判断是否存在
  • 调用first()/last()/get()等取值方法

我用点外卖来类比:你对着菜单点了好几个菜,这只是“下单”,厨房还没开始做;当你第一次看到外卖小哥敲门时,那才是真的执行了。filter()就是在菜单上不断加菜,直到你“吃”(迭代)才开始烹饪。

理解惰性有什么实际意义?举个例子:如果你先todos = Todo.objects.all(),再if todos:判断,再for todo in todos:遍历,看起来写了两遍,其实数据库只执行了一次查询。但如果你不小心在判断和遍历之间打印了两次todos,那就会触发两次查询。真实项目中的性能问题,很多就是从这种不经意的重复求值开始的。

5.2 增删改的基本写法

创建对象最常见的三种写法:

# 方式一:create 一步到位 Todo.objects.create(title="写周报", description="周一上午完成") # 方式二:先实例化,再 save todo = Todo(title="写周报", description="周一上午完成") todo.save() # 方式三:get_or_create,避免重复创建 obj, created = Todo.objects.get_or_create(title="写周报")

方式一和方式二在大部分场景下等价,但方式二的好处是可以在save()之前修改更多字段。方式三适合处理“存在就复用,不存在就新建”的场景,比如用户点了一个关注按钮,你要保证数据库里只有一条关注关系。

更新数据也有两种常见姿势:

# 姿势一:先取出对象,改字段,再 save todo = Todo.objects.get(pk=1) todo.done = True todo.save() # 姿势二:直接对 QuerySet 执行 update Todo.objects.filter(pk=1).update(done=True)

第一种会先查一次再保存,适合需要处理对象上下文的场景;第二种只执行一条 UPDATE 语句,效率更高,适合批量处理。但要注意,update()是直接写在数据库层面的,不会经过模型的save()方法,所以某些需要自定义逻辑的字段不要用这种方式。

删除数据相对简单:

todo = Todo.objects.get(pk=1) todo.delete()

delete()返回的是一个元组(删除总数, {"app_label.ModelName": 删除数量})。这个返回值在调试时很有用,可以确认到底删掉了多少条。另外还可以批量删除:

Todo.objects.filter(done=True).delete()

5.3 delete() 背后的级联逻辑:on_delete 你真的懂了吗

删除操作在 Django 里最值得注意的不是怎么删,而是“删了这一条,会不会连带把别的数据也删了”。

这跟外键的on_delete参数直接相关。比如你要给每个待办事项加一个“分类”:

class Category(models.Model): name = models.CharField(max_length=50) class Todo(models.Model): category = models.ForeignKey(Category, on_delete=models.CASCADE)

这里on_delete=models.CASCADE的意思是:如果某个分类被删了,那所有属于这个分类的待办事项也会被一起删掉。这是 Django 对外键关系最常用的处理方式。

新手常遇到的情况是:删一个分类,结果大量待办事项悄悄没了。这不是 BUG,而是你选择的级联策略的结果。on_delete的常见选项,我整理成了一个速查表:

选项行为适用场景
CASCADE删除关联对象时,连同引用它的对象一起删订单明细跟着订单删,正合适
PROTECT如果有引用存在,禁止删除关联对象分类下面有文章时不允许删分类
SET_NULL关联对象删除后,把外键字段设为 NULL需要保留数据,但解除关联关系;要求该字段设置null=True
SET_DEFAULT关联对象删除后,设为默认值需要兜底到一个默认分类
DO_NOTHING什么都不做数据库自己会有约束,或者你想在应用层处理

这个选择的本质是在“数据完整性”和“操作便利性”之间做权衡。写代码之前先问自己一句:这个分类删了,它下面的待办事项应该跟着消失,还是应该留下来变成“未分类”?答案不同,选型就不同。

5.4 查询写法速查:filter、get、exclude、Q 对象

接下来整理一些最常用到的查询写法,我尽量按“从简单到复杂”排列:

# 取所有 Todo.objects.all() # 过滤 Todo.objects.filter(done=False) Todo.objects.filter(title__contains="周报") # 模糊匹配 Todo.objects.filter(created_at__date="2025-01-01") # 日期匹配 # 排除 Todo.objects.exclude(done=False) # 排序和切片 Todo.objects.order_by("-created_at")[:10] # 取单条 Todo.objects.get(pk=1) # 取某个值 Todo.objects.values_list("title", flat=True) # 判断是否存在 Todo.objects.filter(title="写周报").exists()

有两个地方新手很容易进坑。

第一个是get()。当查询结果多于一条或少于一条时,get()会抛异常:MultipleObjectsReturned或DoesNotExist。所以在不确定数据唯一性的时候,更稳妥的做法是用filter(...).first(),这样即使查不到也不会抛异常,最多返回None。

第二个是“或”逻辑。默认的filter(A, B)是 AND 关系,如果你想表达“标题包含 A 或 标题包含 B”,就需要用到Q对象:

from django.db.models import Q todos = Todo.objects.filter(Q(title__contains="周报") | Q(description__contains="周报"))

Q对象支持|和&操作,对应 SQL 里的 OR 和 AND。这个语法的出现频率比想象中高得多,建议入门就记住,后面做搜索功能时几乎是绕不开的。

5.5 实例:给 Todo 增加“完成”和“删除”功能

理论说了一堆,现在把它们串起来。我们来给待办事项加两个动作:标记完成、删除。

修改todo/views.py:

from django.shortcuts import render, redirect, get_object_or_404 from .models import Todo def todo_list(request): todos = Todo.objects.all().order_by("-created_at") return render(request, "todo/todo_list.html", {"todos": todos}) def toggle_todo(request, pk): todo = get_object_or_404(Todo, pk=pk) todo.done = not todo.done todo.save() return redirect("todo_list") def delete_todo(request, pk): if request.method == "POST": todo = get_object_or_404(Todo, pk=pk) todo.delete() return redirect("todo_list")

注意两个细节。

get_object_or_404是get()的增强版:查到就返回对象,查不到直接返回 404 页面。为什么用它?因为用户可能手抖访问了一个不存在的数据,这时候让页面显示 404 比报一个DoesNotExist异常要友好得多。

删除操作我写了if request.method == "POST"这个判断。这是绝对必须的安全习惯。用 GET 请求触发删除,意味着用户只要在浏览器里打开一个链接就能把数据删掉,搜索引擎爬虫、浏览器预加载都可能触发这种请求,而且删除 URL 会留在历史记录里。所以删除操作必须用 POST,并且通常在模板里以表单形式提交。

更新模板todo/todo_list.html:

{% extends "todo/base.html" %} {% block content %} <h1>待办事项</h1> <ul> {% for todo in todos %} <li> <form action="{% url 'toggle_todo' todo.pk %}" method="post" style="display:inline;"> {% csrf_token %} <button type="submit"> {% if todo.done %}恢复{% else %}完成{% endif %} </button> </form> <span>{{ todo.title }}</span> {% if todo.done %}<span>已完成</span>{% endif %} <form action="{% url 'delete_todo' todo.pk %}" method="post" style="display:inline;"> {% csrf_token %} <button type="submit" onclick="return confirm('确定删除吗?');">删除</button> </form> </li> {% empty %} <li>暂无待办事项</li> {% endfor %} </ul> {% endblock %}

模板里有两处{% csrf_token %},这是 Django 的 CSRF 防护机制。它的作用是为每个表单生成一个随机的令牌,服务器验证这个令牌来确保请求是来自你站点的页面,而不是第三方伪造的。如果你在 POST 表单里漏掉这行,Django 会直接拒绝请求,返回 403 页面。很多新手第一次遇到 CSRF 报错都会懵,其实就是少了这个东西。

对应更新todo/urls.py:

from django.urls import path from . import views urlpatterns = [ path("todos/", views.todo_list, name="todo_list"), path("todo/<int:pk>/toggle/", views.toggle_todo, name="toggle_todo"), path("todo/<int:pk>/delete/", views.delete_todo, name="delete_todo"), ]

<int:pk>会从 URL 里抓取数字,比如/todo/3/delete/会把pk=3传给视图函数。这套流程走下来,你会明显感受到 Django 的套路感很强:URL 配视图,视图用 ORM,模板渲染数据。套路熟了就快了。

6. 管理后台:再进一步,认识 Django Unfold

6.1 默认 Admin 已经很能打

Django 的 Admin 后台到目前已经帮我们省了很大力气。只要在admin.py里注册一下模型,你就拥有了增删改查、分页、搜索、筛选等完整功能。对内部系统来说,这几乎可以被直接拿来当业务后台用。

不过默认 Admin 的界面风格比较“直男”,中规中矩,谈不上好看。如果项目是给客户演示用,或者内部团队对体验有要求,那界面观感就成为一个实际需求。这也是“Django Unfold”这类主题库能火起来的原因。

6.2 Django Unfold:又一个让后台变好看的选择

如果你关注 Django 生态,近几年应该看到过 django-unfold 这个名字。它是一个基于现代设计语言开发的后台主题,安装很直接:

pip install django-unfold

然后在settings.py里把unfold加在django.contrib.admin前面:

INSTALLED_APPS = [ "unfold", "django.contrib.admin", # 其他应用 ]

再把admin.py里的导入改成:

from unfold.admin import ModelAdmin from django.contrib import admin from .models import Todo @admin.register(Todo) class TodoAdmin(ModelAdmin): list_display = ["title", "done", "created_at"]

重启服务刷新后台,界面会立刻变成另一种风格。Unfold 相比默认后台的差异,主要在侧边栏布局、卡片式卡片、深色模式支持等体验细节上。对于想要“后台看起来像现代化产品”的团队来说,投入很小,观感提升巨大。

同类的方案还有 django-jazzmin、django-simpleui 等。选哪个并不重要,关键是你知道后台的可视化层是可以替换的。Admin 底层的权限体系、数据操作能力都没变,换的只是皮。

顺带提一句现在行业里的另一个趋势:很多人开始用 AI Agent 辅助生成 Django 代码。比如描述一个需求,AI 帮你生成模型和视图的骨架。我的态度是可以用,但前提是你自己已经能看懂生成出来的代码。很多来找我请教的新手,用 AI 生成了一个能跑的项目,出了问题却完全没有排查思路,因为根本不知道代码在干什么。工具能帮你省时间,但替代不了理解。

6.3 关于后台安全的一个提醒

后台界面做漂亮了,数据操作也要管住。Django 默认只允许is_staff=True的用户访问 Admin,普通注册用户是进不去的。这是最基本的一道闸门。

另外,默认的 admin 地址是/admin/,如果有人想暴力破解,这个路径等于开门迎客。一个可行的做法是在根路由里把 admin 路径改成一个不显眼的地址:

path("manage/", admin.site.urls),

不过别神化这种“隐藏”的作用。真正的安全靠的是强密码、权限最小化、登录限流这些实打实的机制。改个路径,顶多挡住一些扫描器,解决不了弱密码和权限过大的问题。

7. 新手最容易踩的坑与排查速查

7.1 环境类问题:命令找不到、版本对不上

新手最常见的报错,往往不是代码问题,而是环境问题。我把高频情况整理一下:

现象原因解决方案
django-admin提示“命令不存在”虚拟环境未激活激活虚拟环境,检查.venv/Scripts是否在 PATH
python不是内部或外部命令Windows 未正确配置 Python安装时勾选 Add to PATH,或使用py命令
pip安装后import django失败pip 和 python 不属于同一个环境确认当前在虚拟环境里,用python -m pip而不是裸pip
项目能跑,但django-admin版本和项目对不上多个环境串了尽量只用一个虚拟环境,不要在系统级全局乱装

一个通用排查思路:先执行which python或where python,看看当前解释器路径是不是在你激活的虚拟环境里。这一步能解决一半以上的环境类报错。

7.2 迁移类问题:No changes detected、冲突、丢失

makemigrations常见的一个坑是:你改了models.py,执行python manage.py makemigrations,结果提示No changes detected。这时候先检查INSTALLED_APPS里有没有注册这个 app。之前说过,没注册就不会被扫描到。

另一个情况是多人在同一个项目里开发,各自新增了字段,生成了多个迁移文件,合并时出现依赖冲突。报错会提示你解决冲突,常规手段是运行python manage.py makemigrations --merge,让 Django 帮你生成一个合并迁移。这个不是灵丹妙药,但能处理大多数简单冲突。

还有一类问题是“我改了模型,忘了 makemigrations,直接 migrate”。Django 不会自动追踪你的模型改动,迁移记录的生成永远需要你显式执行命令。我的习惯是:每次改完模型,立刻makemigrations,看到生成了新迁移文件再继续写别的代码,绝不攒到后面。

7.3 URL 和视图类问题:404、500、路由顺序

新手在浏览器看到 404 页面,第一反应是代码写错了,其实经常是路由没匹配上。排查步骤很简单:

  1. 看请求的 URL 是什么,比如/todos/。
  2. 看todo/urls.py里是否写了path("todos/", ...)。
  3. 确认根路由todo_project/urls.py没有把todo.urls注释掉。

还有一个隐蔽问题:路由顺序。如果两条路由的 pattern 都能匹配同一个 URL,Django 会按urlpatterns列表里的顺序从上到下依次匹配,一旦命中就停止。所以更具体的路由要写在更泛用路由的前面,比如path("todo/<int:pk>/delete/", ...)要放在path("todos/", ...)后面还是前面其实影响不大,但如果遇到path("todo/<str:name>/", ...)这种泛用写法,就一定要把更具体的路由放在前面。

500 错误在开发阶段看到时,记住一句话:打开settings.py,确认DEBUG=True。DEBUG 模式下页面会显示完整的异常类型、堆栈和上下文,对排查帮助极大。

7.4 模板和静态文件问题:路径不对、静态 404

模板加载不到时,Django 会报TemplateDoesNotExist。这时最有效的排查是看设置的模板搜索路径。我用 Django 5 之后默认采用 app 目录下的templates/自动发现机制,只要目录结构是todo/templates/todo/todo_list.html,并且 app 在INSTALLED_APPS里注册过,一般都能找到。如果自定义了DIRS,那就要确保你手动配置的路径真实存在。

静态文件(CSS、JS、图片)加载不出来,是另一个高频问题。开发阶段,你需要在模板里先写{% load static %},然后用{% static 'css/style.css' %}这种方式引用,同时在项目根目录创建static/文件夹,并在settings.py里确认:

STATICFILES_DIRS = [ BASE_DIR / "static", ]

你自己的静态文件会跟 Django 自带的管理后台静态文件合并。如果改了但还是 404,先运行python manage.py findstatic css/style.css看一下 Django 实际搜索到了哪个目录。这个命令是排查静态文件问题的神器。

7.5 时区、语言和 CSRF:三个“小”问题

settings.py里LANGUAGE_CODE我一般设成"zh-hans",TIME_ZONE设成"Asia/Shanghai"。特别要注意USE_TZ=True(默认开启)时,Django 会把时间按带时区的格式存入数据库,显示时要转成当前时区。新手常见的现象是:创建的记录比本地时间慢了 8 小时,或者反之。这通常是时区处理不一致导致的,排查思路是统一用 Django 的时区工具django.utils.timezone.now(),不要手动调datetime.datetime.now()去跟时区较劲。

CSRF 报错CSRF verification failed,绝大多数情况是 POST 表单里没有{% csrf_token %}。少数情况是 AJAX 请求需要额外在请求头携带 CSRF token,这个入门阶段先不用管,普通表单带好模板标签即可。

8. 新手心态:把第一篇的“小项目”跑起来,比看十遍文档重要

最后说点我个人的体会。我最早学 Django 的时候,掉进过一个“文档黑洞”的陷阱:总觉得应该把官方 Tutorial 从头到尾读一遍再动手,结果读完一半就没了激情。后来我换了个方式,强迫自己在三天内做一个能用的待办事项应用,不要它多完善,只要能新建、能展示、能删除、能标记状态。做完之后再回头翻文档,很多概念不需要背就自然贯通了。

如果你把这一篇的内容跟着敲完了,现在手里已经有一个能跑的小项目:有后台、有列表页、能新增、能删除、能标记完成。这个体量作为入门第一篇已经很够用了。接下来我建议你试着给它加一个“分类”功能,给每个 Todo 加一个外键关联,然后测试一下on_delete不同选项下删除分类的后果。这个练习能把外键、级联、迁移这些概念一次串起来,比单纯看文档有用得多。

另一个小技巧想分享给你们:尽早把settings.py里的配置拆成development.py和production.py两个文件。入门教程里一般不会提这个,因为单文件在开发阶段最省事。但一旦项目要部署上线,你会发现生产环境的DEBUG=False、数据库连接、静态文件收集配置跟本地开发完全不同。第一篇阶段不用做得很复杂,只需要知道这个方向,等写第二篇、第三篇时我们可以一起把这套结构搭出来。

返回列表