ARTICLE DETAIL

资讯详情

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

Django投票应用实战:从模型到视图的完整开发指南

Django投票应用实战:从模型到视图的完整开发指南 1. 为什么是投票应用一个能完整跑通Django主线的入门项目我见过太多想学Django的人上来就看一堆理论结果发现自己连一个能跑的项目都搭不出来。也见过一些人一上来就想复刻淘宝、抖音搞了几天连路由和模板的关系都没理清直接劝退。如果你问我从零学Django、或者想快速验证自己对Django的理解第一个项目该做什么我大概率还是会推荐你做一个投票应用。投票应用看起来简单但它几乎是Django所有核心功能的最小完整演示模型层Model怎么定义数据表ORM对象关系映射怎么执行查询和删除对象管理后台Admin怎么零代码生成CRUD视图View怎么接收请求并返回响应URL路由怎么分发请求模板Template怎么渲染数据表单怎么处理POST请求还有CSRF防护、数据库迁移、多表关系这些知识点全都覆盖到了。换句话说你把投票应用从头到尾做一遍等于把Django的主干跑通了一遍。之后再去看CBV类视图、DRFDjango REST Framework、Django Channels都是在这个骨架上加东西。这篇文章我会按照我自己实际搭建时的顺序来写不是把官方文档抄一遍而是把我踩过的坑、反复琢磨过的设计原因、以及那些文档里没写但你一定会遇到的问题都补进来。你要做的只是打开终端跟着往下走。每一步我会解释为什么这样做以及如果做错了会出现什么现象。最后你会得到一个能跑起来、能投票、能在后台管理数据的完整应用并且知道下一步该往哪扩展。2. 环境与初始化别跳过虚拟环境也别把项目和应用混为一谈2.1 虚拟环境这一步到底在防什么很多新手觉得虚拟环境麻烦直接用全局Python装个Django就开始干活。短期看是快但只要你手上有两个项目其中一个要Django 4.2另一个要用Django 5.0全局环境就炸了——pip install 会把旧版本覆盖掉另一个项目当场报废。我甚至有次因为全局环境里装了一个高版本的pillow导致一个老项目部署时静态文件处理直接报错排查了一晚上才发现是依赖冲突。所以第一步老老实实建虚拟环境。Windows下用python -m venv venv venv\Scripts\activatemacOS和Linux下用python3 -m venv venv source venv/bin/activate激活之后终端前面会出现一个(venv)前缀说明你已经在虚拟环境里了。这时候再装Djangopip install django装完可以用python -m django --version确认一下版本。我写这篇时用的Django 5.0系列不过你就算用4.2也不会差太多核心逻辑完全一致。2.2 项目Project和应用App的区别很多人一开始就没搞懂Django里有两个概念极其容易混淆项目和应用。我最早学的时候花了很长时间才转过弯来。打个比方项目就像一个站点骨架它包含整个网站的配置、URL入口、全局设置应用则是站点里一个个独立的功能模块。投票就是一个应用用户管理是另一个应用博客又是另一个应用。它们可以被反复移植到不同的项目里这才是Django提倡的可复用性。所以操作顺序是先创建项目再在项目里创建应用django-admin startproject mysite cd mysite python manage.py startapp polls这一步跑完后你的目录结构大致是这样的mysite/ manage.py mysite/ __init__.py settings.py urls.py asgi.py wsgi.py polls/ migrations/ __init__.py admin.py apps.py models.py tests.py views.py注意这里有两个mysite目录外层是项目的根目录内层才是真正的配置文件包。很多新手在这里迷路不知道改哪个文件。后面所有配置修改进的都是内层mysite/settings.py。2.3 settings.py第一轮改动语言、时区、App注册新建的项目默认语言是英文、时区是UTC国内开发第一件事就是把这两个改掉。打开mysite/settings.py把LANGUAGE_CODE en-us TIME_ZONE UTC改成LANGUAGE_CODE zh-hans TIME_ZONE Asia/ShanghaiUSE_TZ默认是True表示启用时区支持。这里有个非常经典的坑Django在启用USE_TZ的情况下存入数据库的时间都是UTC时间渲染到模板时才会根据TIME_ZONE转换。如果你在代码里直接打印datetime.now()会发现时间比北京时间慢8个小时别慌这是正常的。如果你希望数据库里直接存北京时间可以把USE_TZ设为False但生产环境我建议保持True因为国际化的系统里数据库统一存UTC是通用规范北京时间只是显示时区。然后是注册应用。在settings.py的INSTALLED_APPS列表里加一行INSTALLED_APPS [ django.contrib.admin, django.contrib.auth, django.contrib.contenttypes, django.contrib.sessions, django.contrib.messages, django.contrib.staticfiles, polls, # 这一行是新增的 ]不注册应用后面makemigrations会自动忽略polls里的模型这是一个经常让人懵半天的点——代码写完了执行迁移却告诉你没有变化十有八九是这个原因。3. 数据模型设计Question和Choice为什么是两个表以及ORM的删除逻辑3.1 先想清楚关系再动手建模投票应用的核心需求是一个问题Question下面有多个选项Choice用户点了某个选项该选项的票数加一。这就天然形成了一对多的关系一个问题对应多个选项每个选项从属于一个问题。如果用SQL思维你会直接想建两张表通过外键关联。Django的ORM把这一层隐藏掉了你只需要定义两个Python类。打开polls/models.py写成这样from django.db import models class Question(models.Model): question_text models.CharField(max_length200) pub_date models.DateTimeField(date published) def __str__(self): return self.question_text class Choice(models.Model): question models.ForeignKey(Question, on_deletemodels.CASCADE) choice_text models.CharField(max_length200) votes models.IntegerField(default0) def __str__(self): return self.choice_text这里有几个值得琢磨的细节。ForeignKey的第一个参数是关联的模型我用的是Question这个类名而不是字符串Question因为在同一个文件里定义直接引用没问题。如果你要引用另一个应用的模型就需要用appname.ModelName这种字符串形式。on_deletemodels.CASCADE的含义是当一个问题被删除时它下面所有选项也一起删除。这是很自然的级联逻辑。另有一个常见选项是models.SET_NULL表示外键置空这要求你该字段必须设置nullTrue, blankTrue。选择哪个取决于业务语义——投票场景下选项离开问题就没有意义所以用CASCADE合理。votes字段默认值是0IntegerField会自动创建数据库层面的默认约束。__str__方法不是可选的摆设它决定了对象在后台管理列表、ORM查询结果中怎么显示。不写的话你在admin后台看到的全是Question object (1)根本没法用。3.2 makemigrations和migrate生成迁移和应用迁移为什么要分两步定义好模型后执行python manage.py makemigrations polls这一步会生成一个迁移文件在你完全没写任何SQL的情况下它把你的模型变化翻译成Python代码记录在polls/migrations/0001_initial.py里。你可以打开看看里面就是创建question和choice两张表的详细定义。它的意义在于迁移文件是可审查、可版本控制的。团队协作时别人拉下你的代码只需要执行migrate就能生成同样的表结构不用手动去数据库里建表。接着执行python manage.py migrate这一步才是真正把表建进数据库。默认用的是settings.py里配置的SQLite数据库文件是项目根目录下的db.sqlite3。开发阶段用SQLite就够了它是个文件型数据库零配置重启不丢数据而且Django对它的支持最完善。关于ORM执行查询和删除对象这里给出最常见的几条操作。通过python manage.py shell进入交互环境from polls.models import Question, Choice # 创建一个问题 q Question(question_text你最喜欢哪个编程语言, pub_date2024-01-01 12:00:00) q.save() # 查询所有问题 Question.objects.all() # 通过filter筛选 Question.objects.filter(question_text__contains编程) # 删除对象 q.delete()删除操作会级联删除这个问题下的所有选项如果你想验证可以在删除前q.choice_set.all()看看有哪些选项删完再查就空了。顺便说一句关联的反向查询用choice_set不是choices除非你在ForeignKey里指定了related_name。这个细节也是面试题里高频出现的。3.3 给默认数据表也来个说法执行完migrate后你可能会发现数据库里多了一堆表auth_user、django_session等等。这些是Django自带应用认证、会话、权限的表。后台管理的登录功能靠的就是auth_user。也就是说从你执行migrate那一刻起账号体系就已经建好了只是还没人给你演示过而已。4. 管理后台60秒生成一个能用的数据管理系统4.1 创建超级管理员注册模型Django后台是我最喜欢向新手安利的模块。其他框架要写一堆增删改查的代码Django直接给你一个现成的、支持搜索、分页、过滤的后台界面。第一步是创建管理员账号python manage.py createsuperuser按提示输入用户名、邮箱、密码。密码有强度校验太简单会报错但你也可以强行确认开发环境无所谓。然后打开polls/admin.py把模型注册进后台from django.contrib import admin from .models import Question, Choice admin.site.register(Question) admin.site.register(Choice)启动开发服务器python manage.py runserver浏览器打开http://127.0.0.1:8000/admin/用刚才的账号登录就能看到问题和选项两个管理入口。点进去你可以直接添加问题、添加选项、通过外键下拉框选择选项所属问题。整个过程没写一行前端代码。4.2 让后台更好用的三个小改动只做register是最基础的做法实际使用中你会发现两个痛点一是Choice的列表页只显示选项文本分不清每个选项属于哪个问题二是添加一个问题时没法同时内联添加选项必须跑到另一个页面去加很割裂。改进方法是在admin.py里用ModelAdmin和TabularInlinefrom django.contrib import admin from .models import Question, Choice class ChoiceInline(admin.TabularInline): model Choice extra 3 class QuestionAdmin(admin.ModelAdmin): list_display (question_text, pub_date) list_filter (pub_date,) search_fields (question_text,) inlines [ChoiceInline] admin.site.register(Question, QuestionAdmin)代码含义很好理解list_display控制列表页显示的列list_filter是右侧按日期过滤search_fields开启搜索框inlines让添加问题的时候直接在同一页添加3个选项extra3。改完刷新后台体验完全不一样。还有一个细节后台页面顶部的Django administration和Django管理这种默认文案可以通过以下方式修改admin.site.site_header 投票系统后台 admin.site.site_title 投票管理 admin.site.index_title 欢迎使用投票管理系统在很多实际项目里这一步改不改不影响功能但如果是给非技术同事演示一个定制标题瞬间让系统显得正规了许多。5. 视图、路由和模板一个投票请求的完整生命周期5.1 视图函数是怎么把数据变网页的先给投票应用做三个页面首页显示所有问题列表详情页显示问题下的所有选项结果页显示某问题的投票结果外加一个处理投票动作的vote视图。打开polls/views.py写最基础的函数式视图Function Based Viewfrom django.shortcuts import get_object_or_404, render from django.http import HttpResponseRedirect, HttpResponse from django.urls import reverse from .models import Question, Choice def index(request): latest_question_list Question.objects.order_by(-pub_date)[:5] return render(request, polls/index.html, { latest_question_list: latest_question_list, }) def detail(request, question_id): question get_object_or_404(Question, pkquestion_id) return render(request, polls/detail.html, {question: question}) def results(request, question_id): question get_object_or_404(Question, pkquestion_id) return render(request, polls/results.html, {question: question}) def vote(request, question_id): question get_object_or_404(Question, pkquestion_id) try: selected_choice question.choice_set.get(pkrequest.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,)))有几个点值得展开讲。get_object_or_404是一个缩写它是get查询外加Http404异常处理的合并。直接Question.objects.get(pkquestion_id)在查不到时会抛DoesNotExist导致500错误。用get_object_or_404就能优雅地返回404用户看到的是页面不存在而不是服务器报错。request.POST[choice]是从POST表单里取值。为什么用request.POST而不是request.GET因为投票这个动作会改变数据票数加一这类操作按HTTP规范应该走POST。如果你把表单改成GET浏览器地址栏会带着参数刷新页面时会重复提交票数会疯涨。这里还体现了一个极其重要的模式验证-处理-重定向。先用try捕获用户未选选项的情况给出错误提示处理成功后不直接渲染模板而是HttpResponseRedirect重定向到结果页。这样做有个很实际的好处用户按F5刷新结果页只是重新请求结果投票逻辑不会再次执行如果直接渲染结果页刷新就会重新走一遍vote逻辑票数会被重复增加。很多新手写投票功能时票数莫名翻倍往往就是没做这个重定向。5.2 路由配置namespace的作用和include的拆分逻辑views.py只定义了函数还不够还需要告诉Django什么URL对应什么视图。在polls目录下新建一个urls.pyfrom django.urls import path from . import views app_name polls urlpatterns [ path(, views.index, nameindex), path(int:question_id/, views.detail, namedetail), path(int:question_id/results/, views.results, nameresults), path(int:question_id/vote/, views.vote, namevote), ]然后打开项目的mysite/urls.py把polls.urls挂载进来from django.contrib import admin from django.urls import include, path urlpatterns [ path(polls/, include(polls.urls)), path(admin/, admin.site.urls), ]这里有两个新手最容易疑惑的点。第一为什么使用include而不是直接把polls的路径写进主urlpatterns因为include实现了解耦。主URLconf只负责把polls/开头的请求转交给polls.urls至于polls/下面具体有多少路径主文件完全不用知道。将来你把这个应用移植到另一个项目只需要复制polls目录再在另一个项目的URLconf里加一行include(polls.urls)就完事了。第二app_name polls是干什么的它是给URL起了一个命名空间。当你项目里有多个应用每个应用里都有一个叫index的URL名称时不加命名空间就冲突了。加了之后模板和视图里可以用polls:index、polls:detail这种带前缀的写法精确引用。这也是我在vote视图里用reverse(polls:results, args(question.id,))的原因——reverse会解析该命名URL并返回路径例如/polls/1/results/。不用硬编码URL的好处是将来URL路径变了只要不改name代码和模板都不用改。5.3 模板目录与模板语法Django默认在应用目录下的templates子目录里自动找模板。所以你需要创建polls/templates/polls/这样两层目录结构。为什么templates下还要再套一层pollsDjango在多个应用的模板目录是合并搜索的如果不加这层子目录所有应用的模板都放在同一个命名空间里很容易互相覆盖。这是官方推荐的标准做法。创建polls/templates/polls/index.html!DOCTYPE html html head title投票首页/title /head body {% if latest_question_list %} ul {% for question in latest_question_list %} lia href{% url polls:detail question.id %}{{ question.question_text }}/a/li {% endfor %} /ul {% else %} p还没有任何投票问题。/p {% endif %} /body /html模板语法里{% %}是逻辑标签{{ }}是变量输出。{% url polls:detail question.id %}会动态生成详情页URL这正好呼应了前面app_name的用途。然后是detail.html这是表单提交的核心页h1{{ question.question_text }}/h1 {% if error_message %}pstrong{{ error_message }}/strong/p{% endif %} form action{% url polls:vote question.id %} methodpost {% csrf_token %} {% for choice in question.choice_set.all %} input typeradio namechoice idchoice{{ forloop.counter }} value{{ choice.id }} label forchoice{{ forloop.counter }}{{ choice.choice_text }}/labelbr {% endfor %} input typesubmit value投票 /form注意这里有一个{% csrf_token %}这是Django在表单层面自动加入的防跨站请求伪造机制。没有这个标签提交表单会被Django拒绝报403错误。csrf_token会在渲染时生成一个隐藏的CSRF token字段提交时Django校验通过才处理请求。这个机制是Django内置安全体系的亮点很多其他框架需要额外装插件才能做到。最后是results.htmlh1{{ question.question_text }}/h1 ul {% for choice in question.choice_set.all %} li{{ choice.choice_text }} —— {{ choice.votes }} 票/li {% endfor %} /ul a href{% url polls:detail question.id %}再投一次/a到这里创建数据-后台管理-前端展示-用户投票-结果展示的闭环就通了。打开http://127.0.0.1:8000/polls/你应该能看到问题列表点进去可以投票投完会跳到结果页。6. 迁移与数据操作ORM执行查询和删除对象时要避开的细节6.1 一对一、一对多、多对多ORM里如何区分Django的ORM模型关系主要就三种ForeignKey是多对一或一对多用在Choice到Question的关系上。OneToOneField是一对一比如扩展用户表时你有用户基本信息和用户扩展信息两条记录一一对应用OneToOneField可以把两个模型关联到一起。ManyToManyField是多对多典型的场景是文章-标签一篇文章有多个标签一个标签也能对应多篇文章。在投票项目里只有ForeignKey但理解另外两种关系会对后续扩展有很大帮助。比如你后期想给投票增加标签功能一个问题可以贴上多个标签就可以class Tag(models.Model): name models.CharField(max_length50) questions models.ManyToManyField(Question)这样Django会自动生成一张关联表你在后台也能直接通过多选控件维护问题与标签的关系不用自己写任何查询逻辑。6.2 删除对象的两种方式和级联逻辑删除对象最简单的是obj.delete()。但如果你想按条件批量删除可以用 QuerySet 的delete()# 删除所有选项数为0的Choice Choice.objects.filter(votes0).delete()这会返回一个元组(总删除数, {app.Model: 数量})。注意QuerySet.delete()会立即在数据库层面执行不像filter是惰性查询所以执行前最好确认条件或者先.count()看一眼有多少条记录。还有一个常常被忽略的点Django默认不会同步删除数据库里通过外键关联的孤儿数据以外的对象实际上on_deletemodels.CASCADE的级联删除是在ORM层面实现的执行Question.objects.filter(...).delete()时Django会先查出所有关联的Choice再一起删除。如果你在数据库层面手动改了外键约束或者用了原生SQL这个级联逻辑就要小心了。6.3 使用 shell 做假数据和查询验证在开发过程中手工在后台一条条点数据太慢了。我一般用 shell 批量造数据python manage.py shellfrom polls.models import Question, Choice from django.utils import timezone # 循环创建10个问题和若干选项 for i in range(10): q Question.objects.create( question_textf测试问题{i}号, pub_datetimezone.now() ) for j in range(3): Choice.objects.create(questionq, choice_textf选项{j}, votes0)然后用Question.objects.count()确认数量为10用查询条件过滤出含特定关键词的问题Question.objects.filter(question_text__contains测试).count()这些操作熟练后你对ORM的掌握程度会突飞猛进。实际上在生产环境里数据清洗、字段迁移、批量修正这些操作很多时候就是靠这种交互式命令完成的比直接连数据库写SQL更安全也更高效。7. 常见报错的排查逻辑与为什么要养成读报错习惯7.1 页面无法访问类如果你访问http://127.0.0.1:8000/polls/出现404先看路径对不对。我早期经常少写末尾的斜杠Django有一个APPEND_SLASH机制默认是True它会在你没写斜杠时自动重定向到带斜杠的URL。但你如果自定义过URL规则这个机制不一定每次都帮你补上排查时要先看报错信息里Django列出的URLconf内容。如果你访问http://127.0.0.1:8000/时没有显示任何内容那是因为你的URLconf里没有定义根路径。投票应用挂在/polls/下根路径要显示内容要么添加一个自定义path(, views.index)风格的视图要么干脆把polls.urls的path(polls/, ...)改成path(, include(polls.urls))这个操作会让投票应用成为站点首页看你的需求定。7.2 No polls are available类数据库里没有数据如果你正常访问但页面显示空白或还没有任何投票问题那基本是数据库里没有数据。到后台添加几条或者用 shell 造数据。不要怀疑代码写错了先确认数据存在。7.3 TemplateDoesNotExist at /polls/类模板路径错了Django报错信息非常明确它会告诉你在哪个目录下找过模板。一般原因是templates/polls/index.html的层级建错了。记住Django不会自动把每个应用下的templates目录单独隔离它把所有应用声明为app_directories.Loader查找的候选目录所以你在polls/templates/polls/下放文件在其他应用也有同类路径的情况下Django会按INSTALLED_APPS顺序查找到第一个同名模板就停止。为了避免命名冲突polls/templates/polls/这个双层结构是约定俗成的解法。7.4 403 CSRF验证失败类如果你在模板里忘了加{% csrf_token %}提交表单基本都会报错。快速排查确认form标签里有没有这一行确认你的Middleware里CsrfViewMiddleware是否被意外移除一般没人会动它。另外如果你用Postman测试POST请求需要在请求头或表单里带上CSRF token不然也会403这并不代表代码有问题只是Django的安全策略在起作用。7.5 迁移时提示某个字段无法为NULL类比如你给Choice新增了一个非空字段但数据库里已有旧记录迁移时会报错。解决办法是用makemigrations时Django会交互式询问你如何为已有行填充默认值选择提供一次性默认值或者把字段改成带默认值即可。如果嫌麻烦直接在模型定义里指定default...或nullTrue。这些报错其实是你最好的学习材料。不要一看到红字就慌认真读第一行Error Type和第二行Error Message绝大部分问题都能自己定位。我对比过有人动不动就把报错复制到搜索引擎的做法——搜当然能解决但如果养成看完整堆栈再动手的习惯你的排错能力会提升得非常快。8. 把纯函数视图升级成通用视图省代码但要知道代价和适用边界8.1 用ListView和DetailView重写视图Django提供了泛型视图Generic Views本质是封装好的类视图。它高度抽象了查询数据库-渲染模板这个流程。我常用的两个是ListView和DetailView。直接看改写后的views.pyfrom django.views import generic from django.urls import reverse_lazy from .models import Question, Choice 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 class ResultsView(generic.DetailView): model Question template_name polls/results.html看着是不是简洁很多但要注意三个约定ListView默认会查找polls/question_list.html这个模板如果你用了自定义模板名必须在template_name里指定DetailView默认传给模板的变量名是question根据模型名小写化而来如果你想用别的变量名就需要在ListView里设置context_object_name。这样改完以后路由里的path也要稍微变化因为类视图不能直接用函数名注册需要用as_view()path(, views.IndexView.as_view(), nameindex), path(int:pk/, views.DetailView.as_view(), namedetail), path(int:pk/results/, views.ResultsView.as_view(), nameresults), path(int:question_id/vote/, views.vote, namevote),注意DetailView 默认期望URL里传pk不是question_id所以上面的URL参数名要按int:pk来写。8.2 泛型视图的使用边界那么问题来了既然泛型视图省代码为什么前面还要花那么大篇幅讲函数视图因为泛型视图把很多细节隐藏了。你如果一开始就接触ListView理解不了get_queryset为什么自动处理了“分页”、object_list和context之间的关系。函数视图虽然代码多但每一步是显式的对理解Django请求-响应流程帮助更大。更关键的是泛型视图适合展示型页面——比如列表页、详情页、简单表单提交页。遇到复杂业务逻辑比如你需要在投票前做用户权限校验、需要同时操作多个模型的数据、需要动态修改表单字段泛型视图反而需要覆盖一堆方法代码量并不比函数视图少可读性还更差。我的建议是投票应用这种学习项目先用函数视图把流程跑通等理解了原理再切换到泛型视图体验一下少写代码的爽感也看清楚背后发生了什么。实际工作中的小项目我往往混合使用——CRUD用泛型视图特殊逻辑用函数视图两种模式没有高下之分。8.3 分页和搜索结果的小优化如果问题数量多了ListView默认会分出page_obj对象模板里可以这样翻页{% if page_obj.has_previous %} a href?page{{ page_obj.previous_page_number }}上一页/a {% endif %} span第 {{ page_obj.number }} 页共 {{ page_obj.paginator.num_pages }} 页/span {% if page_obj.has_next %} a href?page{{ page_obj.next_page_number }}下一页/a {% endif %}这是一个非常实用的后期优化点。不做分页一旦问题列表超过几十条首页加载会明显变慢用户体感很差。9. 测试与后续扩展从能跑到敢上线9.1 Django原生测试怎么用Django自带一套基于Pythonunittest的测试框架。你不需要额外装pytest也能写测试。在polls/tests.py里加一段最简单的测试import datetime from django.test import TestCase from django.utils import timezone from .models import Question class QuestionModelTests(TestCase): def test_was_published_recently_with_future_question(self): future_question Question(pub_datetimezone.now() datetime.timedelta(days30)) self.assertIs(future_question.was_published_recently(), False)这段测试验证was_published_recently方法对“未来时间”返回False。先跑python manage.py test polls看看测试是否通过。这个过程的收益被你日常开发中随手点几下的验证方式最大差异是测试是可重复的、自动化的而且以后每次改代码都能立刻知道有没有把原来好的功能改坏。对于投票应用这种业务逻辑简单的项目你至少可以写几类测试模型层__str__返回值、默认votes、外键关联视图层访问/polls/返回200、访问详情页返回200、投票后票数变化表单层不选选项提交会重新渲染详情页并带错误信息9.2 静态文件和白名单上线前必须处理的三件事开发环境的runserver会自动处理静态文件但部署到生产环境时不会。你需要把settings.py里的DEBUG改为False然后执行python manage.py collectstatic这个命令会把所有应用下的静态文件CSS、JS、图片收集到一个统一目录STATIC_ROOT。如果没有做这一项你会发现页面样式全部丢失。第二件事是ALLOWED_HOSTS。DEBUGFalse时Django只允许访问你在ALLOWED_HOSTS里列出的主机名。如果你部署在本机测试可以设置ALLOWED_HOSTS [127.0.0.1, localhost]如果用域名访问就把域名加进去。这一步不做线上访问会直接报DisallowedHost异常。第三件事是数据库切换。开发SQLite只能单机使用并发性能差。实际部署要么迁移到PostgreSQL要么用MySQL。切换数据库在Django里很简单——装好对应的数据库驱动改一下settings.py里的DATABASES配置然后重新执行migrate就行。我之前有个小项目数据量不大一直用SQLite后来用户量上来了每秒钟几十次写入数据库时SQLite会频繁报database is locked切到PostgreSQL后彻底安静了。9.3 可以继续扩展的方向投票应用做完了你可以往这些方向加东西都能很好地锻炼Django技能用户系统继承AbstractUser自定义用户模型记录每个用户投过哪些票防止重复投票。这个功能涉及鉴权和数据库关联非常有价值。按问题分类新增一个Category模型Question用ForeignKey关联列表页按照分类过滤。图表展示前端用一个图表库比如Chart.js或ECharts把投票结果做成柱状图或饼图涉及JSON接口的输出可以顺手接触一下JsonResponse。API化用DRF把投票接口开放出来让小程序或App也能调用这是目前企业开发中最常见的方向。缓存优化给首页加内存缓存使用Django的cache_page看看QPS能提高多少体会一下缓存的威力。我在给朋友演示这个项目时最后的固定动作总是让他们自己去加一个新功能哪怕只是给Choice加一个color字段在前端显示出来。因为Django的学习曲线真正陡峭的地方不在于跟着教程敲代码而在于你敢于脱离教程独立增删功能的那一刻——你做成了说明你对这个框架已经不再是照着写而是真的在用它了。
返回列表