首页 / 资讯中心 / 文章详情

Django实战:从零搭建博客系统,打通模型视图模板路由全流程

Django实战:从零搭建博客系统,打通模型视图模板路由全流程 ★ FEATURED ARTICLE
Django这个名字在Web开发圈子里出现频率有多高不用我多说。但很多刚开始接触全栈的朋友最容易卡在“学了一堆概念却不知道一个完整的项目到底怎么串起来”这个环节。所以我写了这篇东西目标很直接用一个博客系统作为载体把Django的模型、视图、模板、路由、后台管理这一整条链路走通。如果你正好学完了Python基础想看看一个真实的Web项目长什么样或者已经在用Django但一直停留在跟着教程敲代码的阶段这篇文章应该能帮你把那些零散的知识点焊接到一起。1. 项目架构与设计思路1.1 为什么选Django来练手全栈市面上能写Web后端的框架很多Flask轻巧灵活FastAPI性能出色但Django有一个别人替代不了的优势它把Web开发里那些高频常用件全部内置了。ORM、表单处理、认证系统、Admin后台、中间件、模板引擎这些在Flask里需要自己找第三方库拼装的东西Django开箱即用而且协同工作得很好。这么说吧用Flask写博客你要先花时间纠结数据库用什么连、Admin后台要不要自己撸一个、登录注册怎么实现。但在Django里这些问题框架已经替你解决了一大半。对于入门全栈的人来说Django能让你把宝贵的精力花在理解“Web应用的运转逻辑”上而不是消耗在“基础设施选型”里。还有一个很实际的考虑Django的MTV模式Model-Template-View模型—模板—视图和传统MVCModel-View-Controller模型—视图—控制器之间的对应关系非常清晰理解了一套以后再接触其他框架心智模型的迁移成本很低。我见过不少开发者是从Django入门后转去学其他语言的普遍反馈是“Web开发的核心思路其实都差不多”这就是框架选得好的长期收益。1.2 博客系统的模块划分和表设计思路既然目标是“入门全栈”那项目的复杂程度就要控制在一个刚好能练手、又不会让人劝退的范围。这个项目我规划了三个核心数据模型文章Post标题、正文、摘要、发布时间、是否置顶等基础字段。分类Category文章所属的知识领域或栏目和文章是一对多关系。标签Tag文章的属性标签和文章是多对多关系。这三个模型基本覆盖了博客最常见的业务场景。分类解决“文章归档到哪”的问题标签解决“文章能被哪些关键词检索到”的问题两者在数据模型上分别体现了一对多和多对多的经典设计对于入门者来说这是两种最需要掌握的关联关系做个博客刚好能同时练到。除了数据模型整个系统按功能还能拆成几个逻辑单元内容展示模块首页列表、详情页、内容管理模块Django自带Admin、检索过滤模块分类标签筛选、分页。这样拆的好处是每个模块都有明确的学习目标出问题时排查范围也清楚。1.3 从“启动项目”到“看清全貌”的路径所有教程都告诉你“装好环境就能写代码”但我个人经验是在动手写第一行代码之前先把Django的请求处理流程在脑子里过一遍比什么都重要。用户在浏览器输入网址、敲回车之后发生了什么Django根据urls.py里的路由配置找到对应的视图函数。视图函数处理请求去数据库取数据这块是ORM在干活。视图函数把数据打包渲染到模板里。模板生成HTML页面返回给浏览器。这个流程听起来简单但它是整个Django应用的核心循环。这篇项目文章里所有的代码本质上都在为这个流程服务。你把这条链路在脑子里跑顺了后面写任何功能都能对号入座想加个新页面加路由、加视图、加模板。想改数据展示改模型、改视图查询、改模板渲染。理解了骨架剩下的都是细节。2. 环境搭建与项目初始化2.1 创建虚拟环境给项目一个独立空间这一步很多新手会觉得啰嗦但它是所有后续操作稳定的前提。虚拟环境的作用是让每个项目拥有独立的Python依赖环境避免不同项目之间因为第三方库版本不一致而互相干扰。实操顺序如下# 创建项目目录并进入 mkdir django-blog cd django-blog # 创建虚拟环境我用的是venvPython自带的方案 python -m venv venv # 激活虚拟环境 # Windows: venv\Scripts\activate # macOS/Linux: source venv/bin/activate # 安装Django当前稳定版本 pip install django激活后命令行前面会出现(venv)的标识看到这个就说明你已经在虚拟环境里了。这里有个实操建议永远不要嫌这步麻烦也不要图省事直接全局装Django。我见过很多人一开始不建虚拟环境项目多了之后A项目需要Django 3B项目需要Django 4升级一个全局包把另一个项目搞崩溃光排这种环境冲突就浪费半天时间。2.2 创建项目和应用分清两个“项目”概念Django里有两个容易混淆的概念Project项目和App应用。一个Project代表一个完整的网站工程一个App代表工程里的一个功能模块。博客系统里文章展示是一个App用户管理是另一个App它们共同构成这个Project。创建命令如下# 创建项目注意最后有个点表示在当前目录生成 django-admin startproject config . # 创建文章应用 python manage.py startapp blog项目名我故意叫config而不是blog这是个老手的习惯项目名代表“这是整个站点的配置中心”而blog才是具体业务模块。这样以后你在这个项目里加“用户系统”“评论系统”时就可以继续创建users、comments等新应用结构很清晰。生成完之后你的目录大概长这样django-blog/ ├── config/ # 项目配置目录 │ ├── settings.py # 全局配置 │ ├── urls.py # 根路由 │ └── ... ├── blog/ # 应用目录 │ ├── models.py # 数据模型 │ ├── views.py # 视图函数 │ ├── admin.py # 后台管理注册 │ └── ... ├── venv/ └── manage.py # 项目管理入口2.3 把应用注册到配置里新手最容易漏的一步创建了App之后很多新手直接开始写代码然后运行起来发现“表不存在”“URL匹配不到”其实都是因为漏了把这个App注册到项目的settings.py里。在INSTALLED_APPS这个列表中加入blog# config/settings.py INSTALLED_APPS [ django.contrib.admin, django.contrib.auth, django.contrib.contenttypes, django.contrib.sessions, django.contrib.messages, django.contrib.staticfiles, # 把自建的 app 加在下面 blog, ]这里建议养成一个习惯自建应用统一加在Django内置应用下面并空一行隔开这样每次维护依赖关系时一目了然。Django内置的那些App其实也都在做事情——admin提供后台管理auth负责用户认证contenttypes是框架内部在用。你写代码时可以不直接碰它们但它们在后台默默支撑着整个系统的运行删掉任何一个都会出问题。2.4 数据库配置默认的SQLite够用吗新项目不配置任何数据库时Django默认使用的就是SQLite一个基于文件的轻量级数据库。配置文件里会有这么一段DATABASES { default: { ENGINE: django.db.backends.sqlite3, NAME: BASE_DIR / db.sqlite3, } }很多入门的朋友会纠结“要不要一开始就配MySQL或PostgreSQL”我的建议很直接练手项目SQLite完全够用。这位朋友SQLite是一个完整的数据库引擎支持标准的SQL语法Django的ORM对它支持也非常完善。等到将来真需要部署上线、面对高并发写操作时再切换到MySQL或PostgreSQL也不迟到时候只需要改这一处配置然后重新执行迁移命令就行。作为入门阶段减少环境依赖把注意力集中在业务逻辑上比什么都重要。确认好配置没问题执行初始化命令python manage.py migrate这个命令的作用是把Django内置的那些数据表用户表、权限表、会话表等创建出来。执行完之后项目的基础骨架就算立起来了。3. 核心功能实现把数据模型和后台敲定3.1 数据模型设计用代码描述博客的“内容结构”在blog/models.py里我们开始定义博客的数据结构。这是整个项目里最需要想清楚的环节因为表结构一旦确定后面所有功能都建立在它之上。改模型的成本会随着项目推进越来越高。# blog/models.py from django.db import models from django.utils import timezone from django.urls import reverse class Category(models.Model): 文章分类例如技术、生活、随笔 name models.CharField(名称, max_length30) class Meta: verbose_name 分类 verbose_name_plural verbose_name def __str__(self): return self.name class Tag(models.Model): 文章标签例如#Django #Python name models.CharField(名称, max_length30) class Meta: verbose_name 标签 verbose_name_plural verbose_name def __str__(self): return self.name class Post(models.Model): 文章主体 title models.CharField(标题, max_length100) content models.TextField(正文) summary models.CharField(摘要, max_length200, blankTrue) category models.ForeignKey( Category, on_deletemodels.CASCADE, verbose_name分类, related_nameposts ) tags models.ManyToManyField( Tag, blankTrue, verbose_name标签, related_nameposts ) created_time models.DateTimeField(创建时间, defaulttimezone.now) modified_time models.DateTimeField(修改时间, auto_nowTrue) is_published models.BooleanField(是否发布, defaultTrue) class Meta: ordering [-created_time] verbose_name 文章 verbose_name_plural verbose_name def __str__(self): return self.title def get_absolute_url(self): return reverse(blog:detail, kwargs{pk: self.pk})这段代码信息量比较大拆开讲几个关键点CharField是短文本字段TextField是长文本字段BooleanField是布尔判断。ForeignKey定义一对多关系一篇文章属于一个分类一个分类下可以有很多篇文章。on_deletemodels.CASCADE表示分类删除时该分类下的所有文章一并删除——这是Django里最常见也最合理的外键约束策略。ManyToManyField定义多对多关系一篇文章可以有多个标签一个标签也可以挂在多篇文章上。class Meta里的ordering [-created_time]让所有文章查询默认按创建时间倒序排列负号表示倒序。这样首页每次查询文章时都自动拿最新文章在前面不用在视图里重复编写排序逻辑。get_absolute_url()是Django的一个习惯写法让文章自己知道自己详情页的URL是哪个后面模板里做跳转时很有用。3.2 生成数据库迁移把设计变成真实的表模型定义好之后需要两步操作才能反映到数据库里# 生成迁移文件相当于数据库的“修改记录” python manage.py makemigrations blog # 应用迁移真正执行把表建出来 python manage.py migrate实际操作中新手常犯一个错误改完模型忘了执行这两步然后发现页面没有变化。记住一个口诀改模型就是改数据库。每次改了models.py必须要先makemigrations再migrate。这条规则在Django的整个生命周期内都适用。如果哪天你改了模型之后执行makemigrations时看到类似Did you forget to specify a default?的提示那是因为你给已有表添加了一个非空字段而数据库里已经存在旧数据Django不知道把这些旧数据的字段填什么。解决办法是给字段指定一个默认值比如default或default0或者允许它为空加上blankTrue, nullTrue。3.3 注册Admin后台连接“代码”和“管理界面”的桥Django最让初学者感到爽的功能就是自带的Admin后台——一个已经完整实现的后台管理界面可以直接对数据库里的数据进行增删改查。但要让它认识我们的博客模型需要在admin.py里注册# blog/admin.py from django.contrib import admin from .models import Category, Tag, Post admin.register(Post) class PostAdmin(admin.ModelAdmin): list_display (title, category, created_time, is_published) list_filter (is_published, category, tags) search_fields (title, content) filter_horizontal (tags,) date_hierarchy created_time admin.site.register(Category) admin.site.register(Tag)这段配置让后台的文章列表页能直接显示标题、分类、创建时间、发布状态这四列还支持按状态和分类筛选、关键词搜索。这些配置在高阶用法里能省掉你写一整套后台界面的时间。注册完之后创建超级用户并启动开发服务器python manage.py createsuperuser # 按照提示输入用户名、邮箱、密码 python manage.py runserver浏览器访问http://127.0.0.1:8000/admin/用刚创建的账号登录你就会看到Post、Category、Tag三个模型的可视化管理页面。在这里面手动创建几篇测试文章配上分类和标签后面写模板时就有数据可以展示了。3.4 视图与路由配置把后台数据送到前端页面模型有了后台管理也通了接下来要做的是把存在数据库里的文章“拿”出来并展示到网页上。这部分的工作由视图函数和URL路由配合完成。在blog/views.py里写两个最简单的视图# blog/views.py from django.views.generic import ListView, DetailView from .models import Post class PostListView(ListView): 文章列表页显示所有已发布文章 model Post template_name blog/post_list.html context_object_name posts paginate_by 5 def get_queryset(self): return super().get_queryset().filter(is_publishedTrue) class PostDetailView(DetailView): 文章详情页显示单篇文章完整内容 model Post template_name blog/post_detail.html context_object_name post这里我直接用Django的通用类视图Class-Based View简称CBV。新手可能更喜欢写函数视图FBV但类视图能少写不少重复代码列表、详情、分页这些常见逻辑Django已经封装好了我们只需要告诉它“模型是哪个、模板是哪份”就行。对应的URL路由需要两层配置。先改项目的根路由config/urls.pyfrom django.contrib import admin from django.urls import path, include urlpatterns [ path(admin/, admin.site.urls), path(, include(blog.urls)), ]然后在blog/目录下新建一个urls.py作为子路由# blog/urls.py from django.urls import path from . import views app_name blog urlpatterns [ path(, views.PostListView.as_view(), namehome), path(post/int:pk/, views.PostDetailView.as_view(), namedetail), ]app_name blog这个命名空间很关键。Django允许不同的App里有相同名字的路由有了app_name模板里用{% url blog:detail post.pk %}就能准确定位到文章详情页不会跟其他App的同名路由搞混。这是个可维护性的问题——项目大了之后没有命名空间的路由是不可想象的。4. 模板系统与前端页面搭建4.1 模板继承高效搭建基础布局Django的模板引擎最高频使用的特性就是模板继承。你可以先把网站的公共骨架头部导航、底部版权、侧边栏写在一个基础模板里然后让每个子页面只专注于自己独有的内容。以blog/templates/blog/base.html为例!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title{% block title %}我的博客{% endblock %}/title /head body header nav a href{% url blog:home %}首页/a /nav /header main {% block content %} {% endblock %} /main footer p一个用Django搭建的博客系统/p /footer /body /html这里{% block title %}和{% block content %}就是两个“占位洞”。子模板通过重写这两个block来输出自己的内容!-- blog/templates/blog/post_list.html -- {% extends blog/base.html %} {% block title %}首页 - 我的博客{% endblock %} {% block content %} h1最新文章/h1 {% for post in posts %} article h2 a href{% url blog:detail post.pk %}{{ post.title }}/a /h2 p{{ post.summary }}/p p{{ post.created_time }}/p p {% for tag in post.tags.all %} span{{ tag.name }}/span {% endfor %} /p /article {% empty %} p还没有文章/p {% endfor %} {% if is_paginated %} div {% 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 %} /div {% endif %} {% endblock %}几个容易理解偏差的点{% for post in posts %}里的posts是我们在类视图里通过context_object_name指定的变量名。如果不指定Django默认会用object_list写模板时容易绕。blog:detail是我们前面配的命名空间加路由名。模板里尽量不要写死URL这样以后改动URL路径时只要urls.py里对应关系不变模板就不用动。分页部分用的是Django自带分页器在模板层的暴露变量page_obj.has_previous、page_obj.has_next。4.2 详情页模板与安全过滤器文章列表页负责“吸引点击”详情页负责“展示内容”。详情页模板长这样!-- blog/templates/blog/post_detail.html -- {% extends blog/base.html %} {% block title %}{{ post.title }} - 我的博客{% endblock %} {% block content %} article h1{{ post.title }}/h1 p发布于 {{ post.created_time }}/p p分类{{ post.category.name }}/p div {{ post.content | linebreaks }} /div /article {% endblock %}这里有个安全细节特别值得强调{{ post.content }}在Django模板里输出时会自动进行HTML转义。意思是如果文章内容里有一串script标签它不会被浏览器当作脚本执行而是被安全地显示成文本。这个自动转义机制对于防止XSS攻击至关重要。所以默认情况下你在模板里输出任何变量Django都在默默替你挡住了一层安全风险。这套机制轻易不要关闭——如果你用某个字段存的是渲染好的HTML想在模板里直接富文本展示可以用|safe但我强烈建议先弄清楚自己在做什么再考虑要不要关掉这个保护。对于这个博客系统内容字段我建议统一用纯文本存储、模板转义输出简单又安全。4.3 静态文件与页面样式页面光秃秃的一片文字肯定不好看但引入样式也非常简单。你可以在blog/static/blog/目录下新建style.css然后在模板头部用{% load static %}标签加载{% load static %} link relstylesheet href{% static blog/style.css %}Django的static模板标签的一大优势在于它会自动拼接STATIC_URL前缀部署上线后把静态文件打包到CDN或独立域名时只需改配置模板不用动。我自己的习惯是入门阶段先用最简单的手写CSS或直接引一个现成的CSS框架目的是先搞清楚“静态文件在Django里是怎么被找到和引用的”而不是一开始就陷入复杂的前端构建流程里。等你把Django全链路的请求响应流程摸透了再回头去折腾复杂的前端工程化会轻松很多。5. 功能扩展与细节打磨5.1 让博客文章支持Markdown渲染纯文本的正文展示比较单调如果能让博主用Markdown语法写文章、发布后渲染成带格式的富文本体验会好很多。这里可以引入markdown这个第三方库pip install markdown然后在详情视图里处理# blog/views.py import markdown from django.shortcuts import get_object_or_404 from django.views.generic import DetailView from .models import Post class PostDetailView(DetailView): model Post template_name blog/post_detail.html context_object_name post def get_object(self, querysetNone): post super().get_object(queryset) post.content markdown.markdown( post.content, extensions[extra, codehilite, toc] ) return post这里有一点必须警惕Markdown渲染后的结果是HTML字符串在模板里输出时必须要用|safe过滤器否则Django会把渲染好的HTML再转义成纯文本显示页面上一堆尖括号。但用|safe的前提是你能确保任写文章的人是可信的——这是要提前想清楚的事。在我的实践里我用了bleach库对渲染后的HTML做了白名单清洗只允许p、h1、h2、code、pre等安全标签把script、iframe这类高风险标签直接过滤掉。5.2 分类和标签筛选的正确姿势列表页只展示全部文章还不够用户通常会点击某个分类查看这个分类下所有文章。实现方式是再增加一个视图然后通过URL参数接收分类ID# blog/views.py from django.views.generic import ListView from .models import Post, Category class CategoryView(ListView): 按分类展示文章 model Post template_name blog/post_list.html context_object_name posts paginate_by 5 def get_queryset(self): self.category get_object_or_404(Category, pkself.kwargs[pk]) return super().get_queryset().filter( categoryself.category, is_publishedTrue ) def get_context_data(self, **kwargs): context super().get_context_data(**kwargs) context[category] self.category return context对应路由path(category/int:pk/, views.CategoryView.as_view(), namecategory)模板里侧边栏可以做一个分类菜单{% for category in category_list %} a href{% url blog:category category.pk %}{{ category.name }}/a {% endfor %}注意逻辑链条URL里拿到分类ID → 视图查询该分类的文章 → 模板渲染列表。这个模式是Django项目里的万金油能解决绝大多数“按条件过滤后展示列表”的需求。标签筛选思路一模一样就不多写了。5.3 分页是必须的吗当文章数量超过二三十篇时分页就不是可选项而是刚需了。Django的分页器用起来很简单。列表视图里我们已经设置了paginate_by 5模板里的上一页/下一页按钮在前面也写好了。关键点是理解分页器内部逻辑page_obj变量会自动传到模板上下文里它携带了当前页码、上一页、下一页、总页数等信息不需要额外写一堆分页逻辑。在我的实际项目里分页样式最终还加了页码数字循环比如展示1 2 3 4 5这种可以直接点击的页码。原理都是围绕page_obj.paginator.page_range这个迭代器做文章理解了基础扩展起来思路是相似的。6. 常见问题与排查技巧实录6.1 模板里拿不到数据先查这三处一次典型的新手报错是页面渲染出来了但文章列表是空的。排查顺序我建议这样来数据库里有没有数据去Admin后台看一眼没有就手动创建几篇。视图里查的数据是不是已发布的会不会被is_publishedFalse过滤掉了模板里的变量名跟视图传出来的一致吗context_object_name有没有拼错这个排查逻辑的本质是从数据源头到展示终端逐层确认数据在哪个环节“丢失”了。我见过太多人一上来就怀疑模板语法有问题其实前两层出问题的概率大得多。6.2 路由匹配报错和404处理技巧NoReverseMatch是Django里常见的一个异常意思是模板里的{% url %}解析不到对应路由。新手看到这个报错容易慌但其实排查方向很固定检查urls.py里的name参数和模板里写的是否一致。检查app_name是否和include里的namespace匹配。检查URL里有没有需要传递的参数比如post.pk模板里是否传了。如果用户访问一个不存在的文章ID比如post/999/Django默认会抛404。但我不想让用户看到默认的报错页可以在详情视图里指定raise_exception或者直接给项目加一个自定义404模板handler404 blog.views.page_not_found再配一个简单的404.html模板页面友好度会好很多。这个细节很多教程不会提但上线之后是真的有用户会手滑点进一个失效链接的。6.3 迁移文件出错、数据库乱了怎么恢复这个排障场景比较“酸爽”但对于新手来说可能迟早会遇到改了模型执行makemigrations时写错了字段类型或者迁移时报了一堆依赖错误。我的处理心得分几种情况本地开发阶段数据不重要最简单粗暴的办法是删掉db.sqlite3和blog/migrations/下除了__init__.py之外的所有迁移文件然后重新执行makemigrations和migrate。数据丢了没关系反正是在测试。服务器已经有正式数据绝对不要删库。正确流程是手动创建新的迁移文件来修正表结构也就是说把改模型的操作拆成两次迁移删除旧字段、添加新字段。操作前先把数据库文件备份出来稳妥第一。这个问题的根因往往是模型设计初期考虑不全。随着经验积累你会慢慢养成一个习惯写模型前先在纸上画一遍表结构把字段类型、关系、空值策略想清楚了再动代码。6.4 开发环境下的性能与安全提醒入门项目通常不会遇到性能瓶颈但有几句提醒值得提前说数据库查询不要“偷懒”用循环。比如在列表页里要展示每篇博客的分类名和标签如果模板里直接写post.category.name每篇文章都会多一次数据库查询。这篇文章只演示了最简单场景真上线数据量大了要在视图里用select_related和prefetch_related提前把关联数据一次性取出来。部署到公网之前务必把settings.py里的DEBUG改成False同时配置好ALLOWED_HOSTS。DEBUGTrue上线等于把你的项目错误详情直接贴在页面上供路过的人围观这在安全上非常危险。数据库连接信息、密钥等敏感配置永远不要硬编码在代码里养成用环境变量的习惯对于以后无论去公司还是在个人项目里都大有好处。写在最后这篇项目文章涉及的代码量不算大但里面每一个环节从设计表结构、配置后台、编写视图到最后模板渲染都是一名Django开发者日常最基础也最关键的工作内容。我个人在实际操作中的体会是Django的能力边界非常宽广但入门最关键的突破口永远是完整地走通一次MTV全流程、搞懂请求是怎么进来又是怎么出去的。这就像学开车你不需要先把发动机原理研究透彻才能上路但你必须先把方向盘、油门、刹车这条链路练熟。如果你照着这篇内容把项目从零到一跑通了下一步可以考虑自己动手加些功能——比如文章评论、浏览量统计、作者账号体系、RSS订阅每一个都是一个独立的小挑战也都是通往更高水平的好台阶。踩坑不要怕我见过的大多数“运行不了”最终都是配置或路径问题解决一次就长一分经验。
阅读完成 · 觉得有帮助?
咨询建站