作为一名长期使用 Django 开发 Web 应用的工程师,我经常遇到新手询问如何系统性地学习 Django。官方教程虽然优秀,但缺乏实际开发中的经验分享。本文将基于官方投票应用教程,结合我多年 Django 开发经验,带你从零开始构建一个完整的投票系统,并分享那些官方文档没告诉你的实战技巧。
这个项目适合以下人群:
我们将从环境搭建开始,逐步实现模型设计、视图开发、模板渲染、表单处理等核心功能,最后还会覆盖自动化测试、静态文件管理和后台定制等高级主题。每个环节我都会补充官方教程中未提及的实用技巧和常见问题解决方案。
在开始项目前,确保你的 Python 环境是 3.8 及以上版本。我强烈建议使用虚拟环境来隔离项目依赖:
bash复制python -m venv venv
source venv/bin/activate # Linux/Mac
venv\Scripts\activate # Windows
安装 Django 时,我习惯指定版本以避免意外升级带来的兼容性问题:
bash复制pip install django==4.2.0
验证安装时,不要仅满足于看到版本号,还应该检查关键依赖是否正确安装:
bash复制python -m django --version
pip list | grep asgiref # 确保ASGI相关依赖存在
注意:如果你同时开发多个 Django 项目,建议使用
pip-tools或poetry管理依赖,可以精确控制每个包的版本。
使用 startproject 命令创建项目时,我推荐添加自定义项目目录名称,保持工作区整洁:
bash复制django-admin startproject mysite djangotutorial
这会在 djangotutorial 目录下创建项目,而不是直接在当前目录生成文件。项目创建后,你应该看到如下结构:
code复制djangotutorial/
├── manage.py
└── mysite/
├── __init__.py
├── asgi.py
├── settings.py
├── urls.py
└── wsgi.py
关键文件作用说明:
manage.py:项目管理入口,所有 Django 命令都通过它执行settings.py:项目核心配置,后续我们会频繁修改urls.py:URL 路由配置入口启动开发服务器时,我通常会指定 IP 和端口,方便多项目同时开发:
bash复制python manage.py runserver 127.0.0.1:8001
几个有用的启动参数:
--noreload:禁用自动重载,调试复杂问题时使用--insecure:强制静态文件服务(DEBUG=False 时有用)在 settings.py 中,有几个关键配置需要立即调整:
python复制# 添加允许访问的host
ALLOWED_HOSTS = ['localhost', '127.0.0.1']
# 时区设置(中国开发者建议修改)
TIME_ZONE = 'Asia/Shanghai'
踩坑提醒:开发时如果遇到 "Invalid HTTP_HOST header" 错误,就是因为 ALLOWED_HOSTS 没有正确配置。
Django 的项目(project)与应用(app)概念容易混淆。简单来说:
创建应用时,我习惯使用有明确业务含义的名称:
bash复制python manage.py startapp polls
创建后需要在 settings.py 中注册应用:
python复制INSTALLED_APPS = [
...
'polls.apps.PollsConfig', # 完整路径写法更规范
]
投票系统的核心模型是问题和选项,对应 Question 和 Choice 类。在 models.py 中定义时,我会添加详细的字段参数和元数据:
python复制class Question(models.Model):
question_text = models.CharField(
verbose_name='问题内容',
max_length=200,
help_text='请输入不超过200个字符的问题描述'
)
pub_date = models.DateTimeField(
verbose_name='发布日期',
auto_now_add=True # 自动设置创建时间
)
class Meta:
verbose_name = '投票问题'
verbose_name_plural = '投票问题'
ordering = ['-pub_date'] # 默认按发布时间降序
def __str__(self):
return self.question_text
class Choice(models.Model):
question = models.ForeignKey(
Question,
on_delete=models.CASCADE,
verbose_name='关联问题',
related_name='choices' # 显式设置反向关系名
)
choice_text = models.CharField(
verbose_name='选项内容',
max_length=200
)
votes = models.IntegerField(
verbose_name='得票数',
default=0
)
def __str__(self):
return f"{self.question.question_text} - {self.choice_text}"
模型设计时的经验分享:
verbose_name,方便后台管理界面使用related_name,避免后期冲突db_index=True 提升性能class Meta 添加模型级配置首次迁移前,建议先检查迁移文件内容:
bash复制python manage.py makemigrations --dry-run --verbosity 3
确认无误后再生成真实迁移文件:
bash复制python manage.py makemigrations polls
应用迁移时,可以使用 --plan 查看将要执行的 SQL:
bash复制python manage.py migrate --plan
重要提示:迁移文件应该纳入版本控制,但不要手动修改生成的迁移文件!
在 views.py 中,我习惯将业务逻辑拆分为小函数,每个函数只做一件事:
python复制from django.shortcuts import get_object_or_404, render
from .models import Question
def index(request):
latest_questions = Question.objects.order_by('-pub_date')[:5]
context = {
'latest_question_list': latest_questions,
'title': '最新投票问题'
}
return render(request, 'polls/index.html', context)
def detail(request, question_id):
question = get_object_or_404(
Question.objects.prefetch_related('choices'),
pk=question_id
)
return render(request, 'polls/detail.html', {'question': question})
视图开发技巧:
prefetch_related 优化关联查询get_object_or_404 简化对象获取和错误处理Django 模板语言(DTL)虽然简单,但功能强大。我的模板组织习惯:
code复制polls/
├── templates/
│ └── polls/
│ ├── base.html # 基础模板
│ ├── index.html # 列表页
│ ├── detail.html # 详情页
│ └── results.html # 结果页
在 base.html 中定义通用结构:
html复制<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>{% block title %}投票系统{% endblock %}</title>
{% block css %}{% endblock %}
</head>
<body>
<header>
<h1>Django 投票系统</h1>
</header>
<main>
{% block content %}{% endblock %}
</main>
<footer>
<p>© {% now "Y" %} 我的投票应用</p>
</footer>
</body>
</html>
在子模板中使用继承:
html复制{% extends "polls/base.html" %}
{% block title %}{{ title }}{% endblock %}
{% block content %}
<ul>
{% for question in latest_question_list %}
<li>
<a href="{% url 'polls:detail' question.id %}">
{{ question.question_text }}
</a>
</li>
{% empty %}
<li>暂无投票问题</li>
{% endfor %}
</ul>
{% endblock %}
模板开发经验:
{% url %} 模板标签,避免硬编码 URL{% empty %} 分支{% now %} 显示动态日期include 拆分复杂模板在 detail.html 中,表单处理需要特别注意 CSRF 防护:
html复制<form action="{% url 'polls:vote' question.id %}" method="post">
{% csrf_token %}
<fieldset>
<legend>{{ question.question_text }}</legend>
{% for choice in question.choices.all %}
<div>
<input type="radio" name="choice"
id="choice{{ forloop.counter }}"
value="{{ choice.id }}">
<label for="choice{{ forloop.counter }}">
{{ choice.choice_text }}
</label>
</div>
{% endfor %}
</fieldset>
<button type="submit">投票</button>
</form>
对应的视图处理逻辑:
python复制from django.http import HttpResponseRedirect
from django.urls import reverse
def vote(request, question_id):
question = get_object_or_404(Question, pk=question_id)
try:
selected_choice = question.choices.get(
pk=request.POST['choice']
)
except (KeyError, Choice.DoesNotExist):
return render(request, 'polls/detail.html', {
'question': question,
'error_message': "请选择一个选项"
})
else:
selected_choice.votes += 1
selected_choice.save()
return HttpResponseRedirect(
reverse('polls:results', args=(question.id,))
)
表单处理要点:
reverse() 生成 URL,而不是硬编码Django 的通用视图可以大幅减少样板代码。改造后的视图:
python复制from django.views import generic
class IndexView(generic.ListView):
template_name = 'polls/index.html'
context_object_name = 'latest_question_list'
def get_queryset(self):
return Question.objects.order_by('-pub_date')[:5]
class DetailView(generic.DetailView):
model = Question
template_name = 'polls/detail.html'
def get_queryset(self):
return Question.objects.prefetch_related('choices')
class ResultsView(generic.DetailView):
model = Question
template_name = 'polls/results.html'
通用视图使用技巧:
template_name,避免默认路径get_queryset 优化查询get_context_data 添加额外上下文在 tests.py 中,我习惯按功能模块组织测试类:
python复制from django.test import TestCase
from django.utils import timezone
from .models import Question
import datetime
class QuestionModelTests(TestCase):
def test_was_published_recently_with_future_question(self):
time = timezone.now() + datetime.timedelta(days=30)
future_question = Question(question_text="未来问题", pub_date=time)
self.assertIs(future_question.was_published_recently(), False)
def test_was_published_recently_with_old_question(self):
time = timezone.now() - datetime.timedelta(days=2)
old_question = Question(question_text="旧问题", pub_date=time)
self.assertIs(old_question.was_published_recently(), False)
def test_was_published_recently_with_recent_question(self):
time = timezone.now() - datetime.timedelta(hours=23)
recent_question = Question(question_text="最近问题", pub_date=time)
self.assertIs(recent_question.was_published_recently(), True)
测试编写建议:
timezone.now() 而不是 datetime.now()视图测试需要使用 Django 测试客户端:
python复制class PollsViewsTests(TestCase):
def setUp(self):
self.question = Question.objects.create(
question_text="测试问题",
pub_date=timezone.now()
)
def test_index_view_with_no_questions(self):
Question.objects.all().delete()
response = self.client.get(reverse('polls:index'))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "暂无投票问题")
self.assertQuerysetEqual(response.context['latest_question_list'], [])
def test_detail_view_with_future_question(self):
future_question = Question.objects.create(
question_text="未来问题",
pub_date=timezone.now() + datetime.timedelta(days=30)
)
url = reverse('polls:detail', args=(future_question.id,))
response = self.client.get(url)
self.assertEqual(response.status_code, 404)
视图测试要点:
setUp 准备测试数据静态文件组织建议:
code复制polls/
├── static/
│ └── polls/
│ ├── css/
│ │ └── style.css
│ ├── js/
│ │ └── scripts.js
│ └── images/
│ └── background.png
在 style.css 中添加响应式设计:
css复制/* polls/static/polls/css/style.css */
body {
font-family: Arial, sans-serif;
line-height: 1.6;
margin: 0;
padding: 20px;
background: #f5f5f5 url("../images/background.png");
}
@media (max-width: 768px) {
body {
padding: 10px;
}
.poll-question {
font-size: 1.2em;
}
}
静态文件使用技巧:
collectstatic在 admin.py 中,可以高度定制管理界面:
python复制from django.contrib import admin
from .models import Choice, Question
class ChoiceInline(admin.TabularInline):
model = Choice
extra = 3
fields = ['choice_text', 'votes']
readonly_fields = ['votes']
@admin.register(Question)
class QuestionAdmin(admin.ModelAdmin):
fieldsets = [
(None, {'fields': ['question_text']}),
('日期信息', {
'fields': ['pub_date'],
'classes': ['collapse']
}),
]
inlines = [ChoiceInline]
list_display = [
'question_text',
'pub_date',
'was_published_recently'
]
list_filter = ['pub_date']
search_fields = ['question_text']
date_hierarchy = 'pub_date'
def get_queryset(self, request):
return super().get_queryset(request).prefetch_related('choices')
后台定制经验:
@admin.register 装饰器更简洁fieldsets 组织字段更清晰list_display 添加自定义方法列get_queryset 优化查询安装配置步骤:
bash复制pip install django-debug-toolbar
settings.py:python复制INSTALLED_APPS = [
...
'debug_toolbar',
]
MIDDLEWARE = [
...
'debug_toolbar.middleware.DebugToolbarMiddleware',
]
INTERNAL_IPS = ['127.0.0.1']
python复制from django.urls import include, path
urlpatterns = [
...
path('__debug__/', include('debug_toolbar.urls')),
]
Debug Toolbar 使用技巧:
常见优化手段:
python复制# 不好的写法:N+1查询问题
questions = Question.objects.all()
for q in questions:
print(q.choices.all())
# 好的写法:使用prefetch_related
questions = Question.objects.prefetch_related('choices')
for q in questions:
print(q.choices.all())
python复制from django.core.cache import cache
def get_questions():
key = 'latest_questions'
result = cache.get(key)
if not result:
result = Question.objects.order_by('-pub_date')[:5]
cache.set(key, result, timeout=60*15) # 缓存15分钟
return result
python复制from django.core.paginator import Paginator
def listing(request):
question_list = Question.objects.all()
paginator = Paginator(question_list, 10) # 每页10条
page_number = request.GET.get('page')
page_obj = paginator.get_page(page_number)
return render(request, 'list.html', {'page_obj': page_obj})
性能优化要点:
select_related 和 prefetch_related 优化关联查询关键设置调整:
python复制# settings.py
DEBUG = False
ALLOWED_HOSTS = ['yourdomain.com', 'www.yourdomain.com']
# 数据库配置
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'mydatabase',
'USER': 'mydatabaseuser',
'PASSWORD': 'mypassword',
'HOST': '127.0.0.1',
'PORT': '5432',
}
}
# 静态文件配置
STATIC_ROOT = '/var/www/example.com/static/'
STATIC_URL = '/static/'
# 安全配置
SECURE_HSTS_SECONDS = 31536000 # 1 year
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
上线前必须检查:
这个基础投票系统可以进一步扩展:
我在实际项目中发现,Django 的灵活性和扩展性足以支撑从简单应用到复杂系统的各种场景。关键在于遵循 Django 的设计哲学,合理组织代码结构,并在适当的时候引入第三方包来扩展功能。