Django动态个人主页实战:从模型设计到Admin后台完整指南
第三次作业下来题目是“用Django做一个动态个人主页”。说实话刚看到这个题目时我脑子里浮现的还是前面HTML课上的老套路——写一个index.html塞几张个人照片再排个版收工。但“动态”这两个字才是这题真正的考点。你要交付的不是一个写死的静态页面而是让主页里的文章、项目、标签全部从数据库里读取能在后台增删改查前台自动跟着变。这篇东西我会完整走一遍从空项目到能交作业的整个过程包括Model设计、视图路由、模板渲染、Admin后台以及我这个过程中踩过的最典型的几个坑——mysqlclient安装失败、重定向传值、StreamingHttpResponse的文件下载参数。无论你是正在做同款作业还是想用Django搭一个自己的主页都可以直接照着做。1. 先拆题动态个人主页到底要交什么东西1.1 “动态”二字的真正含义很多人的作业做偏了根本原因是一开始就没理解“动态”。动态主页的意思是页面内容不是写死在HTML里的而是浏览器每次发起请求时Django从数据库里把数据查出来再用模板渲染成一个完整的HTML返回给你。这就牵出了Django的核心架构——MTVModel-Template-View。Model负责定义数据结构和操作数据库Template负责展示View负责业务逻辑。你访问主页时完整链路是浏览器请求URL → Django的URLconf找到对应的View → View通过ORM查询数据库 → 把查询结果塞进Context → 模板渲染成HTML返回。这个链路贯穿整个作业先把它刻在脑子里后面每一步都是在给它填肉。如果你只看懂了“Django能写网页”没看懂这个请求链路那作业做出来往往就是一个套了Django壳的静态站很容易被一眼看穿。1.2 功能拆解与数据模型预设计一个合格的动态个人主页至少要有这么几个模块个人简介、文章列表博客、项目展示、文章详情页、分类或标签。其中个人简介可以做成后台可修改的配置文章和项目则明显是典型的列表详情结构。在动手写任何代码之前我强烈建议先做数据模型预设计。你不需要画多正规的ER图但一定要在纸上把表列出来。我当时设计的核心就是三张表文章表Article、分类表Category、标签表Tag。关系也很直观一篇文章属于一个分类ForeignKey但可以有多个标签ManyToManyField。模型关键字段用途Articletitle, content, category, tags, created_at, is_published主页的文章/博客内容Categoryname, slug文章分类一对多Tagname文章标签多对多先把模型设计出来再去做页面你会发现自己写的每一行代码都在为这个模型服务思路会清晰得多。很多同学一上来先写HTML模板写到一半发现数据没地方放回头再造表白白返工。作业本来就赶时间这种浪费最不值。2. 从零搭建工程骨架项目、App、模板目录一路配好2.1 环境准备与初始化命令环境方面我建议用虚拟环境别嫌麻烦。直接装在系统Python里装几个项目之后就乱成一锅粥了。# 创建并激活虚拟环境 python -m venv venv # Windows: venv\Scripts\activate # Linux/macOS: source venv/bin/activate # 安装Django pip install django mysqlclient # 创建项目注意后面的点号表示在当前目录生成manage.py django-admin startproject mysite . # 创建App python manage.py startapp home这里有个很多新手会问的点为什么有了project还要创建app简单理解project是整个网站的配置中心负责settings、根URL、WSGI这些全局东西app才是真正干活的业务模块。“个人主页”这种场景把所有功能都塞进一个app里是完全合理的没必要为了炫技拆成三四个app。作业讲究的是清晰不是复杂。创建完后项目结构大致是这样mysite/ ├── manage.py ├── mysite/ │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py └── home/ ├── admin.py ├── apps.py ├── models.py ├── views.py ├── migrations/ └── tests.py2.2 settings.py里的关键配置第一次跑python manage.py runserver之前我会建议先把settings.py里几项核心配置改好避免后面来回折腾。# settings.py INSTALLED_APPS [ django.contrib.admin, django.contrib.auth, django.contrib.contenttypes, django.contrib.sessions, django.contrib.messages, django.contrib.staticfiles, home, # 自己创建的app要加进来 ] LANGUAGE_CODE zh-hans TIME_ZONE Asia/Shanghai DATABASES { default: { ENGINE: django.db.backends.sqlite3, NAME: BASE_DIR / db.sqlite3, } } TEMPLATES [ { BACKEND: django.template.backends.django.DjangoTemplates, DIRS: [BASE_DIR / templates], # 项目级模板目录 APP_DIRS: True, ... }, ] STATIC_URL static/ STATICFILES_DIRS [BASE_DIR / static] MEDIA_URL /media/ MEDIA_ROOT BASE_DIR / mediaLANGUAGE_CODE和TIME_ZONE不改的话后台界面是英文的文章时间默认是UTC差8个小时。数据库这里先用SQLite开发调试完全够用。如果你打算交作业时用MySQL展示开发阶段也别急着上后文第7节会专门讲mysqlclient的坑。2.3 模板目录与静态资源的组织方式模板和静态文件的目录规划直接影响你后面写页面时的路径查找逻辑。我的习惯是项目根目录下建templates和static因为主页的几个页面是跨模块的公共页面放在项目级目录更合适。mysite/ ├── templates/ │ └── home/ │ ├── base.html │ ├── index.html │ └── article_detail.html └── static/ ├── css/ └── js/模板查找的原则是Django会先查DIRS配置的目录再按APP_DIRS去每个app的templates子目录里找。所以templates/home/index.html这种路径写出来渲染时填home/index.html。把页面的HTML文件按app或者功能再分一层子目录是很好的习惯不然以后页面多了templates根目录一堆同名index.html谁是谁都分不清。3. 数据模型把主页内容变成数据库里的结构化记录3.1 文章、分类、标签的Model设计模型是整个作业的地基。地基稳不稳看你字段设计合不合理。我当时写的home/models.py是这个样子from django.db import models from django.utils import timezone class Category(models.Model): name models.CharField(分类名称, max_length50) slug models.SlugField(URL标识, max_length50, uniqueTrue) class Meta: verbose_name 分类 verbose_name_plural 分类 def __str__(self): return self.name class Tag(models.Model): name models.CharField(标签名称, max_length30) class Meta: verbose_name 标签 verbose_name_plural 标签 def __str__(self): return self.name class Article(models.Model): title models.CharField(标题, max_length200) category models.ForeignKey(Category, verbose_name分类, on_deletemodels.CASCADE) tags models.ManyToManyField(Tag, verbose_name标签, blankTrue) content models.TextField(正文) created_at models.DateTimeField(创建时间, defaulttimezone.now) updated_at models.DateTimeField(更新时间, auto_nowTrue) is_published models.BooleanField(是否发布, defaultTrue) views models.PositiveIntegerField(阅读数, default0) class Meta: verbose_name 文章 verbose_name_plural 文章 ordering [-created_at] def __str__(self): return self.title说几个容易被忽略的细节。第一正文用TextField而不是CharFieldCharField有长度上限文章内容一长就报错。第二ForeignKey一定带上on_deleteDjango 2.0之后这是必填参数了不写跑迁移直接报错。第三blankTrue要配合verbose_name用标签可以为空这样新建文章时不用强制选标签。第四ordering [-created_at]让默认查询结果按创建时间倒序省得每个视图里都写order_by。3.2 迁移到底在做什么Model定义好之后就是两条命令的事python manage.py makemigrations home python manage.py migrate我见过太多同学卡在“我改了models.py为什么还报字段不存在”这种问题上。原因就一个——没执行迁移。makemigrations是把Model的变化生成一个迁移文件相当于一份“数据库变更记录”migrate才是真正把变更应用到数据库里。这两步是前后关系只做一步都不行。如果迁移中途改过字段导致出错一个偷懒但有效的办法是开发阶段直接删掉db.sqlite3文件和home/migrations目录下的迁移文件重新makemigrations和migrate。作业阶段数据量小这个操作完全可行但千万注意正式项目别这么干。3.3 先在Admin里把数据准备起来模型建好后第一件事不是写页面而是去后台把数据录进去。做法是在home/admin.py里注册模型from django.contrib import admin from .models import Article, Category, Tag admin.site.register(Article) admin.site.register(Category) admin.site.register(Tag)然后创建超级用户、启动服务python manage.py createsuperuser python manage.py runserver访问http://127.0.0.1:8000/admin/登录后就能看到三张表的增删改查界面。这一步的价值是把“内容维护”和“代码开发”解耦。你先往后台录三五篇测试文章后面写视图和模板时马上就有真实数据可看不用临时往代码里塞假数据。这是很多教程不会明说但效率极高的工作习惯。4. 视图与路由一次页面请求的完整旅程4.1 URL配置与命名路由是Django的“门卫”所有请求先进它这。项目根路由mysite/urls.py负责分发我建议把home相关的URL写在自己的app里再通过include引进来# mysite/urls.py from django.contrib import admin from django.urls import path, include urlpatterns [ path(admin/, admin.site.urls), path(, include(home.urls)), ]# home/urls.py from django.urls import path from . import views app_name home urlpatterns [ path(, views.index, nameindex), path(article/int:pk/, views.article_detail, namearticle_detail), path(category/int:pk/, views.category_articles, namecategory_articles), ]app_name home是URL命名空间。有了命名空间模板里用{% url home:index %}就能反查出完整URL以后改路径也不用一个个去模板里替换。int:pk尖括号语法是path转换器匹配整数并作为参数传给视图函数这是新版本Django的主推写法老式的正则re_path除非有复杂的URL需求否则没必要用。4.2 首页视图查询、排序、分页首页的核心逻辑是查出所有已发布的文章按时间倒序分页展示。函数视图写法如下# home/views.py from django.shortcuts import render, get_object_or_404 from django.core.paginator import Paginator from .models import Article def index(request): article_list Article.objects.filter(is_publishedTrue) paginator Paginator(article_list, 5) # 每页5篇 page_number request.GET.get(page) page_obj paginator.get_page(page_number) return render(request, home/index.html, {page_obj: page_obj}) def article_detail(request, pk): article get_object_or_404(Article, pkpk, is_publishedTrue) article.views 1 article.save(update_fields[views]) return render(request, home/article_detail.html, {article: article})这里有个容易写错的点Paginator.get_page()和Paginator.page()的区别。page(1)遇到不存在的页码会直接抛PageNotAnInteger或EmptyPage异常而get_page()会自动容错页码不是数字时返回第一页超出范围时返回最后一页。作业演示时用户手滑在URL后面加个?page100用get_page()就不会当场白屏。另外阅读数加一这里save(update_fields[views])是很有用的细节。它告诉Django只更新views这一个字段避免整行数据被重写也避免并发时覆盖其他字段的更新。作业里体现这种细致的地方老师一眼就能看出来你是有意识地在写Django而不是照抄代码。4.3 详情页、分类页与删除操作get_object_or_404(Article, pkpk, is_publishedTrue)这句话干了三件事按主键查文章、确认文章是已发布状态、查不到就直接返回404页面。不要在视图里自己写try...except去捕获DoesNotExist这个快捷函数就是干这个的。分类页的写法跟首页几乎一样只是多了一层过滤def category_articles(request, pk): articles Article.objects.filter(category_idpk, is_publishedTrue) paginator Paginator(articles, 5) page_obj paginator.get_page(request.GET.get(page)) return render(request, home/index.html, {page_obj: page_obj, category_id: pk})至于删除操作Django ORM的删除很简单Article.objects.filter(pkpk).delete()或者实例方法article.delete()。但作业里的文章删除我建议直接放进Admin后台做前台不开放删帖入口安全又省事。除了删除查询的几个高频写法也顺手列一下作业里大概率用得到# 条件查询 Article.objects.filter(category__name技术) # 跨表查询 Article.objects.exclude(is_publishedFalse) # 排除 Article.objects.filter(title__icontainsDjango) # 模糊查询不区分大小写 Article.objects.all()[:3] # 取前三条4.4 函数视图还是类视图Django里查列表、查详情这种事类视图有现成的ListView和DetailView代码确实更短。但我个人建议第一次做作业时老实写函数视图。理由很直接函数视图的请求-响应流程是一行行看得到的出错了容易排查类视图的魔法太多报错时你根本不理解它内部在哪一步出了问题。对比项函数视图 FBV类视图 CBV代码量多但直观少但隐式逻辑多学习曲线平缓陡峭适合场景新手作业、逻辑复杂的页面标准CRUD、快速开发调试难度低逐行可断点高需熟悉MRO和mixin等函数视图写熟了能一眼看出它对应的页面渲染逻辑了再去过渡到类视图会顺畅很多。作业阶段不推荐为了显得高级硬上CBV。5. 模板层如何在HTML里优雅地展示数据库内容5.1 模板继承base.html避免重复劳动个人主页再简单也至少有导航栏、页脚、内容区三块。如果每个页面都复制一遍导航栏代码后面想改一个链接就要改所有页面。模板继承就是干这个的{# templates/home/base.html #} !DOCTYPE html html langzh-hans head meta charsetUTF-8 title{% block title %}我的个人主页{% endblock %}/title {% load static %} link relstylesheet href{% static css/style.css %} /head body nav a href{% url home:index %}首页/a a href#关于我/a a href#项目/a /nav main {% block content %} {% endblock %} /main footer© 我的个人主页/footer /body /html子模板里只需要写自己独有的部分公共结构全部由base.html提供{% extends home/base.html %} {% block title %}首页 - 我的个人主页{% endblock %} {% block content %} !-- 页面特有内容 -- {% endblock %}{% extends %}必须放在子模板的第一行前面不能有任何内容包括空格和注释这是模板语法一个很隐蔽的坑。5.2 列表页与详情页的模板写法列表页的核心是一个for循环把视图里传过来的page_obj对象列表渲染出来{% for article in page_obj %} article h2a href{% url home:article_detail article.pk %}{{ article.title }}/a/h2 p classmeta{{ article.created_at|date:Y-m-d }} · {{ article.category.name }}/p p{{ article.content|truncatechars:80 }}/p /article {% empty %} p还没有发布任何文章/p {% endfor %}模板里访问模型字段和调用方法都用点号article.pk、article.category.name可以直接链式取到关联表的值这是Django模板语言对新手最友好的地方。过滤器用管道符|date:Y-m-d格式化时间truncatechars:80截断正文到80个字符这些都是高频操作先记下这三个date、truncatechars、linebreaks写详情页时正文的换行就靠linebreaks把换行符转成p标签。详情页要显示正文原文content是TextField模板里直接{{ article.content|linebreaks }}就行。注意linebreaks一定要加不然你在后台写的换行在页面里全部挤成一段。这就是“数据库里有换行页面上没换行”的经典问题。5.3 分页导航与URL反转分页导航的代码放在列表页底部{% 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 %}这里要说清楚一个问题为什么链接里用?page2这种查询参数而不是设计成/page/2/这种路径因为分页的URL是跟列表页挂钩的查询参数可以复用当前URL你从分类页跳到第二页URL还是/category/1/?page2视图里仍然能拿到category_id的上下文。如果写成独立路径你就得把category_id再拼进路由里徒增复杂度。还有一点模板里所有的链接都应该用{% url %}标签通过路由的name反查而不是手写死路径。手写/article/3/这样的路径一旦改了路由参数名全站链接全挂。6. Admin后台让改内容这件事不再依赖写代码的人6.1 自定义列表字段与筛选器默认注册的Article在后台只是个简单的列表点进去编辑。但对文章这种字段多的模型不加定制会非常难用。强烈建议至少写成这样# home/admin.py from django.contrib import admin from .models import Article, Category, Tag admin.register(Article) class ArticleAdmin(admin.ModelAdmin): list_display (title, category, is_published, views, created_at) list_filter (category, is_published) search_fields (title, content) list_editable (is_published,) date_hierarchy created_at list_per_page 10 admin.register(Category) class CategoryAdmin(admin.ModelAdmin): prepopulated_fields {slug: (name,)}每一项配置都是针对一个实际痛点的list_display让你在列表页直接看到标题、分类、阅读数不用点进详情list_filter右侧出现筛选栏按分类和发布状态筛数据search_fields开启搜索框list_editable让你直接在列表页勾选是否发布不用进详情页prepopulated_fields是给slug字段用的后台录入分类名时自动生成URL标识对动态主页这种靠URL区分内容的场景非常实用。6.2 后台界面美化的两条路线Django原生Admin的界面谈不上好看“能用但丑”是共识。作业如果想在界面上加点分有两条路线。路线一改模板。覆盖django/contrib/admin/templates/admin/base_site.html把它复制到项目自己的templates/admin/目录下再修改站点标题和CSS引用。好处是不引入额外依赖坏处是改不深顶多换换颜色和文案。路线二上第三方库。django-simpleui和django-jazzmin是两个主流方案安装后加进INSTALLED_APPS重启服务就能看到全新的后台界面。以simpleui为例pip install django-simpleuiINSTALLED_APPS [ simpleui, django.contrib.admin, ... ]我的看法是作业阶段没必要在后台美化上投入太多时间。Admin是给你自己维护数据用的老师真正看的是前台效果和代码逻辑。花两小时调后台样式不如花两小时把前台页面打磨一下。用simpleui这种十分钟能装完的方案就够了别在这一块恋战。7. 踩过的坑和解决过程mysqlclient、重定向传值、文件下载7.1 mysqlclient安装失败从报错到解决的完整链路如果你决定把作业数据库配置成MySQL大概率会遇到这个一上来就卡住的问题pip install mysqlclient # 报错大致是error: command gcc failed ... mysql.h not found这个报错的原因是mysqlclient不是纯Python包它需要编译C扩展编译时要去找MySQL客户端的头文件。系统里没有这个头文件就会在编译环节挂掉。按系统区分解决# Ubuntu/Debian sudo apt install libmysqlclient-dev pkg-config pip install mysqlclient # CentOS sudo yum install mysql-devel gcc pip install mysqlclientWindows下更麻烦因为需要预编译的Visual Studio环境。两个替代方案一是直接下载对应Python版本的whl文件后用pip安装二是绕开mysqlclient改用pymysqlpip install pymysql# mysite/__init__.py import pymysql pymysql.install_as_MySQLdb()install_as_MySQLdb()的作用是把pymysql伪装成MySQLdb让Django的django.db.backends.mysql引擎以为用的是mysqlclient实际调用的是pymysql。这是纯Python方案不用编译但性能比mysqlclient差生产环境不推荐。作业演示完全够用。7.2 重定向之后怎么把数据带过去messages框架的正确用法作业里经常会遇到这种场景前台做了一个“给我留言”的表单提交成功后要跳回首页同时提示“留言成功”。很多人第一反应是render渲染一个带参数的模板但提交成功后刷新页面会重复提交表单这就是经典的POST后刷新问题。正确的做法是redirect但redirect是新的请求原本的POST数据已经丢了。这时要用Django的messages框架# 视图里 from django.contrib import messages from django.shortcuts import redirect def leave_message(request): if request.method POST: # ... 保存留言逻辑 messages.success(request, 留言提交成功我会尽快回复你) return redirect(home:index)模板里通过循环展示消息{% if messages %} ul {% for message in messages %} li class{{ message.tags }}{{ message }}/li {% endfor %} /ul {% endif %}这里要理解一个关键点messages能跨请求传递本质是因为Django把消息存到了session里success()写入页面渲染展示后再从session中清除。所以INSTALLED_APPS里的django.contrib.sessions和django.contrib.messages两个app都必须开着且中间件配置不能动。如果你发现消息渲染不出来先检查这两个app是不是被注释掉了。7.3 StreamingHttpResponsecontent_type和content_disposition的坑如果你给个人主页加了一个“下载我的简历”或者“下载附件”的功能就会碰到Django的文件下载。针对大文件下载官方推荐用StreamingHttpResponse流式响应from django.http import StreamingHttpResponse def download_resume(request): file_path media/resume.pdf def file_iterator(file_path, chunk_size8192): with open(file_path, rb) as f: while True: chunk f.read(chunk_size) if not chunk: break yield chunk response StreamingHttpResponse(file_iterator(file_path), content_typeapplication/pdf) response[Content-Disposition] attachment; filenameresume.pdf return response这个视图里两个响应头参数是重灾区。content_type是HTTP响应头Content-Type告诉浏览器这串字节流是什么类型。application/pdf是PDFapplication/octet-stream是通用二进制流。如果content_type设错了比如一个PDF文件却设成text/html浏览器可能直接乱码或者触发下载。Content-Disposition是决定“内联展示”还是“附件下载”的关键。inline表示浏览器能打开就直接预览attachment则是强制下载。这个参数必须用response[Content-Disposition] ...这种方式设置不是构造函数参数。我第一次写的时候误把content_disposition写进构造函数里结果文件下载头一直没生效浪费了半天排查。还有个更隐蔽的坑如果文件名是中文比如个人简历.pdf直接放进header里在部分浏览器下会乱码。稳妥做法是使用RFC 5987规范的编码格式from urllib.parse import quote filename 个人简历.pdf response[Content-Disposition] fattachment; filename*UTF-8{quote(filename)}我个人在实际操作中的一个经验是写完下载功能后分别用Chrome、Edge、Safari各测一遍。尤其注意检查中文文件名、大文件断点续传、以及流式响应是否完整关闭文件句柄。流式响应配合生成器能做到边读边发不会把整个文件一次性加载进内存——这也是它在文件下载场景下比FileResponse更值得写进作业的一个亮点。回到这个作业本身。动态个人主页的难点从来不在某个单一技术上而在于把模型、视图、模板、后台这四层完整串起来形成一个“后台改数据、前台同步变”的闭环。我在做这个作业时最大的体会就是每写一层都要想着上一层的接口是不是对得上。先做好数据模型再写视图最后套模板一层层往下推整个过程其实是很有节奏感的。按照上面这个流程走下来你交上去的作业至少是一个结构清晰、能自圆其说的Django项目而不是一个披着Django外衣的静态页面。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →