多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Django博客系统从零搭建实战:MTV模式、数据库设计与Waitress+Nginx部署

Django博客系统从零搭建实战:MTV模式、数据库设计与Waitress+Nginx部署 最近帮一个没写过 Python 的朋友从零搭了一套 Django 博客系统整个流程走下来踩了不少坑也沉淀了不少经验。正好手头这个项目告一段落我把整个过程完整复盘一遍从环境准备、项目初始化到 MTV 模式的代码落地、数据库设计再到静态文件处理和 Windows 下的 waitress nginx 部署全部写成一篇可以直接照着操作的文章。如果你正准备学习 Django或者想快速拥有一个属于自己的博客系统这篇文章应该能帮你少走很多弯路。我会尽量把每个关键选择背后的“为什么”讲透而不是简单丢出一堆命令。毕竟网上教程一抓一大把但真正能把“为什么这样做”说清楚的真不多。1. 搭建前想清楚的三件事动手写代码之前我建议你先花 10 分钟想清楚三个问题博客要做什么、技术方案怎么选、数据怎么设计。这三个问题想明白了后面基本是水到渠成的事。1.1 项目定位与需求拆解很多人一说“搭博客系统”上来就想搞用户注册、评论、点赞、私信、后台管理恨不得做个完整的内容平台。我的建议是第一版千万别贪多核心需求就三个——文章展示、分类归档、后台发布。这才是博客系统的“最小闭环”。以我这次项目为例需求清单很简单前台能看到文章列表支持按分类筛选点击文章标题能进入详情页内容用 Markdown 格式展示后台用 Django Admin 管理文章、分类和标签支持静态资源图片、CSS、JS的正常加载后续能方便地加搜索、分页、阅读量统计为什么第一版不做用户注册和评论因为那些牵扯到用户系统、权限验证、反垃圾机制复杂度会高好几个量级。先用 Django Admin 管理内容等博客跑起来、内容有一定积累后再按需扩展这才是务实的路径。我见过太多新手项目死在“完美主义”上——功能规划了一大堆结果连最基本的文章发布都还没跑通。1.2 技术选型为什么是 Django这个项目选 Django核心原因是它“全家桶”式的设计。ORM、模板引擎、Admin 后台、表单处理、认证系统全都内置你不需要为了一个博客去拼凑 Flask SQLAlchemy Jinja2 Flask-Admin Flask-Login 这一大串东西。Django 把这些东西全部打包好了而且彼此之间配合得非常默契。Django 的 MTV 模式Model-Template-View也很好理解Model 管数据Template 管展示View 管业务逻辑。这个模式天然适合博客这种内容型项目——文章是数据Model网页是展示Template处理请求和业务规则的地方就是视图View。你只需要把每一层的东西放到对应位置代码结构自然就清晰了。版本选择上我建议直接用 Django 4.2 LTS。LTS 版本会获得长期维护比追最新版稳定得多。我第一次搭项目时图新鲜用了当时刚出的版本结果第三方库兼容性问题一堆后来老老实实换回 LTS省心不少。1.3 数据库设计三张表就够了博客系统的数据库设计不需要太复杂我这次用了三张核心表文章表Post、分类表Category、标签表Tag。分类和文章是多对一关系一篇文章只能属于一个分类标签和文章是多对多关系一篇文章可以有多个标签一个标签也能挂在多篇文章下。还有一个容易被忽略的点为什么要给分类和文章都加一个 slug 字段因为 URL 用英文短横线比用中文标题或自增 ID 更友好。比如文章标题是“我的第一篇博客”slug 是 my-first-blog那 URL 就是/post/my-first-blog/一眼就能看出内容主题。如果直接用数字 IDURL 就成了/post/1/既不直观也不利于分享传播。字段类型上正文内容用 TextField标题用 CharField(max_length200)发布时间用 DateTimeField阅读量用 PositiveIntegerField 并设置默认为 0。状态字段我用了一个 choices 选项区分“草稿”和“已发布”这样写了一半的文章不会在首页被游客看到。2. 环境准备与项目初始化环境这块看起来简单但坑往往就藏在细节里。我见过太多朋友在第一步就卡住原因无非是 Python 版本不对、Django 装错环境、虚拟环境没建好。2.1 Python 版本与虚拟环境配置首先说版本。Django 4.2 支持 Python 3.8 到 3.12我这次用的是 Python 3.10运行很稳定。建议你别用 3.7 及以下的版本了很多新语法和特性都不支持遇到问题都找不到人帮你。装好 Python 后第一件事是建虚拟环境。这一步无数新手会跳过觉得“我直接 pip install 到全局不就行了省事”。我刚开始也有这个想法直到有一次在系统 Python 里装了某个库的测试版把另一个项目跑崩了才明白虚拟环境的价值。虚拟环境相当于给你每个项目单独开了一个“药物隔离病房”这个项目里装了什么包、什么版本跟其他项目完全隔离互不干扰。Windows 下的操作很简单在项目目录里打开终端python -m venv venv venv\Scripts\activate激活后终端前面会出现(venv)字样这就代表你已经进入虚拟环境了。Linux 或 macOS 下命令是source venv/bin/activate思路一样。2.2 安装 Django 并创建项目和应用激活虚拟环境后安装 Djangopip install django如果想指定版本可以写成pip install django4.2.*这样会安装 4.2 系列的最新补丁版本。安装完成后用两个命令创建项目和应用django-admin startproject blog_project cd blog_project python manage.py startapp blog这里有两个概念要分清项目project和应用app。项目是整个网站的配置中心管理 settings、URL 入口应用是具体功能模块比如博客系统的文章、评论、用户都可以拆成不同的 app。我这个项目就建了一个 blog 应用所有博客相关的代码都写在里面。创建完成后打开 blog_project/settings.py这一步有几个必改的地方# settings.py LANGUAGE_CODE zh-hans TIME_ZONE Asia/Shanghai USE_TZ True INSTALLED_APPS [ django.contrib.admin, django.contrib.auth, django.contrib.contenttypes, django.contrib.sessions, django.contrib.messages, django.contrib.staticfiles, blog, # 记得把新建的 app 加进来 ]语言改成中文、时区改成上海时间一个是让 Django Admin 后台显示中文界面一个是让文章发布时间的存储和展示符合咱们的习惯。如果把 USE_TZ 设为 False数据库存的就是本地时间调起来省事一些但推荐还是保持 True更规范。2.3 数据库迁移与创建超级管理员Django 默认使用 SQLite 数据库对博客这种规模的项目完全够用而且零配置省去安装 MySQL 或 PostgreSQL 的麻烦。初始化数据库的命令python manage.py migratemigrate 会把你 INSTALLED_APPS 里所有应用的模型映射成数据库表。这个命令跑完你打开项目目录会发现多了一个 db.sqlite3 文件那就是你的数据库。然后创建超级管理员账号用于登录后台管理博客内容python manage.py createsuperuser按照提示输入用户名、邮箱、密码。这里注意 Django 对密码强度有要求太简单会提示你重新输入。你可以在提示“是否跳过密码验证”时输入 y 强制使用弱密码但我不建议这么做后台账号还是上点心比较好。最后跑一下开发服务器验证环境是否正常python manage.py runserver浏览器访问 http://127.0.0.1:8000/能看到一个火箭升空页面不同版本界面略有差异就说明 Django 已经跑起来了。3. MTV 模式落地从模型到页面环境没问题后就要开始写代码了。这一部分是整个项目最核心的活儿。Django 的 MTV 模式在博客系统里的体现很典型我先把数据模型写好然后写视图函数处理业务逻辑最后用模板渲染页面。一步一步来完全不需要赶。3.1 模型层实现写好你的数据基石打开 blog/models.py把刚才设计的三张表落地。这里直接把可运行的完整代码放出来from django.db import models from django.contrib.auth.models import User from django.utils import timezone from django.urls import reverse class Category(models.Model): name models.CharField(分类名称, max_length100) slug models.SlugField(URL标识, uniqueTrue) class Meta: verbose_name 分类 verbose_name_plural 分类 def __str__(self): return self.name class Tag(models.Model): name models.CharField(标签名称, max_length100) slug models.SlugField(URL标识, uniqueTrue) class Meta: verbose_name 标签 verbose_name_plural 标签 def __str__(self): return self.name class Post(models.Model): STATUS_CHOICES [ (draft, 草稿), (published, 已发布), ] title models.CharField(标题, max_length200) slug models.SlugField(URL标识, uniqueTrue) author models.ForeignKey(User, verbose_name作者, on_deletemodels.CASCADE) category models.ForeignKey(Category, verbose_name分类, on_deletemodels.CASCADE) tags models.ManyToManyField(Tag, verbose_name标签, blankTrue) content models.TextField(正文内容) excerpt models.CharField(摘要, max_length200, blankTrue) status models.CharField(状态, max_length10, choicesSTATUS_CHOICES, defaultdraft) views models.PositiveIntegerField(阅读量, default0) created_at models.DateTimeField(创建时间, auto_now_addTrue) updated_at models.DateTimeField(更新时间, auto_nowTrue) published_at models.DateTimeField(发布时间, defaulttimezone.now) class Meta: ordering [-published_at] verbose_name 文章 verbose_name_plural 文章 def __str__(self): return self.title def get_absolute_url(self): return reverse(blog:post_detail, args[self.slug])有几个细节说明一下。on_deletemodels.CASCADE的意思是如果作者或分类被删除那对应的文章也一并删除。这是 Django 2.0 之后的强制要求必须显式声明删除行为避免数据库里出现“孤儿数据”。auto_now_add和auto_now的区别也值得记一下前者只在创建时写入当前时间后者每次保存都会更新为当前时间。所以创建时间用 auto_now_add更新时间用 auto_now这个组合是 Django 项目里的标准做法。写完模型后执行python manage.py makemigrations blog python manage.py migratemakemigrations 生成迁移文件migrate 把迁移应用到数据库。以后每次修改模型字段都要重复这两步这是 Django 的“数据库版本控制”机制。3.2 视图层选择函数视图还是类视图Django 视图有两种写法函数视图FBV和类视图CBV。新手教程里经常混着讲容易把人搞晕。我的建议是博客系统这种场景先别纠结用函数视图把逻辑写清楚等到你熟悉了请求处理流程再尝试类视图也不迟。我的首页文章列表视图这么写from django.shortcuts import render, get_object_or_404 from django.core.paginator import Paginator from .models import Post, Category def post_list(request, category_slugNone): queryset Post.objects.filter(statuspublished) if category_slug: category get_object_or_404(Category, slugcategory_slug) queryset queryset.filter(categorycategory) paginator Paginator(queryset, 5) page_number request.GET.get(page, 1) posts paginator.get_page(page_number) return render(request, blog/post_list.html, { posts: posts, categories: Category.objects.all(), })这段逻辑其实说了三件事第一只把状态为“已发布”的文章展示出来草稿不进列表第二如果有分类参数就按分类过滤第三用 Paginator 做分页每页显示 5 篇。分页这个功能很重要文章一多你会发现没有分页的页面根本没法看。文章详情视图def post_detail(request, slug): post get_object_or_404(Post, slugslug, statuspublished) post.views 1 post.save(update_fields[views]) return render(request, blog/post_detail.html, {post: post})每次访问详情页阅读量加 1。这里用了update_fields[views]意思是最新一次 UPDATE 只更新 views 字段避免把 updated_at 也改掉也减少数据库写操作。这种小细节单个请求看着无所谓流量大了差别还是很明显的。3.3 路由配置URL 到底怎么映射Django 2.0 以后的路由写法和老版本不一样统一用 path() 函数。项目根路由 blog_project/urls.py 负责把 URL 分发到具体应用from django.contrib import admin from django.urls import path, include urlpatterns [ path(admin/, admin.site.urls), path(, include(blog.urls)), ]然后在 blog 应用里新建 urls.pyfrom django.urls import path from . import views app_name blog urlpatterns [ path(, views.post_list, namepost_list), path(category/slug:category_slug/, views.post_list, namepost_list_by_category), path(post/slug:slug/, views.post_detail, namepost_detail), ]路径里slug:slug这种语法是 URL 转换器。slug 转换器只匹配字母、数字、短横线和下划线组成的字符串正好对应我们模型里 SlugField 的格式。写错了类型Django 会直接返回 404不会继续执行视图函数这其实是一种很好的数据校验机制。app_name blog 是为了在模板中做 URL 反向解析时区分同名路由。比如模板里写{% url blog:post_detail post.slug %}Django 就能准确定位到 blog 应用下名为 post_detail 的路由顺利生成文章详情页 URL。3.4 模板层模板继承与页面渲染模板是 MTV 里的 T 层职责是展示数据。我这里先说模板继承。写页面时最忌讳每个 HTML 文件都完整复制一遍导航栏、CSS 引用、页脚那样改一次样式要动十几个文件。用模板继承这些公共部分只写一次。templates/base.html 是基础模板!DOCTYPE html html langzh-hans head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title{% block title %}我的博客{% endblock %}/title {% load static %} link relstylesheet href{% static css/style.css %} /head body header nav a href{% url blog:post_list %}首页/a {% for category in categories %} a href{% url blog:post_list_by_category category.slug %}{{ category.name }}/a {% endfor %} /nav /header main {% block content %} {% endblock %} /main footer© 我的博客/footer /body /html注意 for 循环里遍历的 categories 变量是在视图里通过 render 传给模板的。如果每个页面都手动传确实有点烦但好处是逻辑透明你可以清楚知道每个模板拿到了哪些数据。后续如果嫌麻烦可以用自定义模板标签或上下文处理器来做但第一版没必要上这些技巧。子模板 post_list.html 继承 base.html{% extends base.html %} {% block title %}首页 - 我的博客{% endblock %} {% block content %} {% for post in posts %} article h2a href{% url blog:post_detail post.slug %}{{ post.title }}/a/h2 p{{ post.excerpt }}/p p发布于 {{ post.published_at|date:Y-m-d }} | 分类: {{ post.category.name }} | 阅读: {{ post.views }}/p /article {% empty %} p还没有发布任何文章。/p {% endfor %} div classpagination {% if posts.has_previous %} a href?page{{ posts.previous_page_number }}上一页/a {% endif %} span第 {{ posts.number }} / {{ posts.paginator.num_pages }} 页/span {% if posts.has_next %} a href?page{{ posts.next_page_number }}下一页/a {% endif %} /div {% endblock %}模板里|date:Y-m-d是 Django 内置过滤器作用是把日期格式化成“年-月-日”的样式。{% empty %}是 for 循环的特殊分支列表为空时显示提示语句这个语法在写列表页时特别常用。详情页模板 post_detail.html 相对简单把文章标题、分类、正文、标签展示出来即可。正文如果存的是 Markdown 格式还需要在视图里用 markdown 库转换成 HTML转好后用{{ post_content_html|safe }}输出。那个|safe过滤器是告诉 Django“这个内容是可信的不用转义 HTML”只有在你已经对内容做过安全处理的情况下才能用。3.5 后台管理Django Admin 注册模型Django Admin 是这个框架最亮眼的功能之一。你需要做的只是在 blog/admin.py 里注册一下模型后台管理界面就出来了from django.contrib import admin from .models import Post, Category, Tag admin.register(Post) class PostAdmin(admin.ModelAdmin): list_display (title, category, status, views, published_at) list_filter (status, category, tags) search_fields (title, content) prepopulated_fields {slug: (title,)} actions [make_published] admin.register(Category) class CategoryAdmin(admin.ModelAdmin): list_display (name, slug) admin.register(Tag) class TagAdmin(admin.ModelAdmin): list_display (name, slug)这里有个小细节我需要单独强调prepopulated_fields {slug: (title,)}意思是后台填写标题时slug 字段会自动根据标题生成。中文标题转 slug 不一定好看但至少比手动输入方便生成后你也可以手动修改。actions [make_published]是自定义后台操作可以批量把草稿文章标记为已发布def make_published(self, request, queryset): queryset.update(statuspublished) make_published.short_description 将选中文章标记为已发布登录后台地址是 /admin/你之前创建的超级管理员账号在这里登录。4. 静态文件处理与图片显示这部分单独拿出来重点讲因为太多人第一次写 Django 项目时就栽在这里。热搜词里有一条“vscode写img标签 在django的static文件中显示不了”这几乎是每个新手的必经坑。4.1 为什么图片加载不出来Django 处理静态文件和普通 HTML 页面不一样。如果你直接在模板里写img srcimages/logo.png altlogo浏览器会去找当前路径下的 images 目录但 Django 默认不会把项目里的静态文件映射到 URL 上。你需要在模板里这么写{% load static %} img src{% static images/logo.png %} altlogo{% static %}模板标签的作用是生成完整的静态文件 URL。它会把 STATIC_URL 配置和传入的路径拼接起来输出类似/static/images/logo.png的访问地址。4.2 STATIC_URL 与 STATICFILES_DIRS 配置settings.py 里跟静态文件有关的配置有两处常用STATIC_URL /static/ STATICFILES_DIRS [ BASE_DIR / static, ]STATIC_URL 是静态文件在 URL 中的前缀可以理解为访问入口STATICFILES_DIRS 告诉 Django 在开发模式下要去哪些目录找静态文件。BASE_DIR / static 意思是项目根目录下的 static 文件夹。目录结构这样建blog_project/ ├── blog/ ├── static/ │ ├── css/ │ │ └── style.css │ ├── js/ │ │ └── main.js │ └── images/ │ └── logo.png ├── templates/ └── manage.py4.3 开发环境与生产环境的差异开发环境下Django 自带静态文件服务你只要把文件放进正确目录、用{% static %}标签引用runserver 就能正常展示。但生产环境不是这样。生产环境下Django 官方不建议用 runserver 跑业务也不负责托管静态文件静态文件应该交给 Nginx 这类服务器软件处理。所以部署时需要先执行一条命令python manage.py collectstatic这个命令会把所有 app 和 STATICFILES_DIRS 下的静态文件统一收集到一个目录 STATIC_ROOT 下然后交给 Nginx 对外提供访问。settings.py 里要加STATIC_ROOT BASE_DIR / staticfiles如果你 DEBUGFalse 但没有执行 collectstatic或者没有正确配置 Nginx 的静态文件路由后台管理页面就会丢失所有样式整个页面变成纯文本堆砌。我在部署阶段就遇到过这个问题后面会详细说排查方法。4.4 本地图片上传与 MEDIA 配置博客文章配图一般有两种方式一是用外链图片直接写完整 URL二是把图片上传到服务器Django 提供访问路径。第二种需要用 MEDIA 相关配置MEDIA_URL /media/ MEDIA_ROOT BASE_DIR / media开发环境下还需要在 urls.py 里加上一段静态服务配置from django.conf import settings from django.conf.urls.static import static urlpatterns [ # ... 其他路由 ] if settings.DEBUG: urlpatterns static(settings.MEDIA_URL, document_rootsettings.MEDIA_ROOT)模板里引用上传的图片时用{{ post.image.url }}获取完整地址。这块建议第一版先不做等博客跑起来再按需求加上避免一开始就陷入图片处理的泥潭。5. 进阶功能与扩展方向博客系统跑通后有几项功能建议按需扩展。我按优先级排序把每项功能的价值和实现方式讲清楚你自己判断要不要做。5.1 搜索功能用 Q 对象实现多字段模糊查询搜索是博客的高频需求实现起来也很简单。用 Django 的 Q 对象可以对多个字段做 OR 条件查询同时匹配标题、正文和摘要from django.db.models import Q def post_search(request): keyword request.GET.get(q, ) posts Post.objects.filter( Q(title__icontainskeyword) | Q(excerpt__icontainskeyword) | Q(content__icontainskeyword), statuspublished ) return render(request, blog/search_results.html, {posts: posts, keyword: keyword})icontains是不区分大小写的包含查询相当于 SQL 里的 LIKE。三个字段用|或连接只要任意一个字段包含关键词就能匹配到。这是最朴素的实现方式文章量不大时完全够用。等你有几千篇文章了再考虑全文检索引擎比如 Django 自带的 PostgreSQL 全文搜索或者 Elasticsearch。5.2 阅读量统计与热门文章阅读量字段在模型里已经建好了post_detail 视图里也做了累加。想让首页展示热门文章只需要在查询时按 views 倒序排列popular_posts Post.objects.filter(statuspublished).order_by(-views)[:5]这里[:5]是 Python 的切片语法放到 ORM 查询里会被翻译成 SQL 的 LIMIT 5只取前五条性能上没问题。需要注意的是切片必须放在查询链的最后面取完切片后就不能再继续链式调用了。5.3 RSS 订阅给博客加个信息出口RSS 虽然现在用得少但作为博客的“基础设施”有总比没有好。Django 内置了 syndication 框架写个 Feed 类就能搞定from django.contrib.syndication.views import Feed from django.urls import reverse from .models import Post class LatestPostsFeed(Feed): title 我的博客 - 最新文章 link / description 博客最新发布的文章 def items(self): return Post.objects.filter(statuspublished)[:10] def item_title(self, item): return item.title def item_link(self, item): return reverse(blog:post_detail, args[item.slug]) def item_description(self, item): return item.excerpt然后在 urls.py 里注册 URL 就能用。这个功能适合对技术有洁癖、想把博客做得完整的朋友成本很低收益也算正向。5.4 标签云与分类导航分类导航已经在 base.html 里遍历出来了。标签云是在侧边栏里展示所有标签点击标签能看到所有带该标签的文章。这在模型层面已经支持了因为文章和标签是多对多关系只需要增加一个按标签过滤的视图即可。关于多对多字段有一个性能上的坑需要提醒在模板里循环输出文章时如果每篇文章都要访问post.tags.all()会产生大量 SQL 查询也就是 N1 问题。解决办法是在视图查询时用prefetch_related(tags)posts Post.objects.filter(statuspublished).prefetch_related(tags).select_related(category)select_related 针对外键查询prefetch_related 针对多对多查询。这两个方法都是让 Django 一次性把关联数据查出来避免循环中反复访问数据库。这个优化在数据量少时感受不明显文章超过几十篇后响应速度的差异就出来了。6. 部署上线waitress nginx 方案开发环境跑通只是第一步博客要给别人访问就得部署上线。传统方案是 Linux 服务器 Gunicorn Nginx但如果你手头是 Windows 服务器或者只是想在本地局域网里让朋友访问一下那 waitress Nginx 的组合更合适。这也是热搜词里反复出现的搭配。6.1 为什么选 waitresswaitress 是一个纯 Python 实现的 WSGI 服务器最大的好处是跨平台Windows 上跑得非常好。而 Gunicorn 虽然性能优秀但对 Windows 支持很有限经常出现装不上、跑不起来的情况。如果你在 Windows 上用 Djangowaitress 是最靠谱的选择。安装pip install waitress启动waitress-serve --port8000 blog_project.wsgi:application这条命令会启动一个生产级 WSGI 服务监听 8000 端口。到这里你的博客已经可以通过本机 IP 或域名访问了。但此时静态文件还是没处理的状态Django 在 DEBUGFalse 时不会帮你托管静态文件所以需要 Nginx 接手。6.2 Nginx 反向代理配置Nginx 的定位是反向代理服务器和静态文件服务器。它为外网请求提供入口把动态请求转发给 waitress 处理把静态文件请求直接返回本地文件。在 Nginx 配置目录下新建一个站点配置文件核心配置如下server { listen 80; server_name your_domain_or_ip; location /static/ { alias C:/path/to/blog_project/staticfiles/; } location /media/ { alias C:/path/to/blog_project/media/; } location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }注意 alias 指向的是 collectstatic 收集后的目录不是开发用的那个 static 文件夹。这两个目录别搞混了我见过有人配置完后台样式丢失排查一圈发现是 alias 配到了旧的 static 目录结果文件不存在Nginx 返回 404。部署前记得在 settings.py 里做最后检查DEBUG False ALLOWED_HOSTS [your_domain_or_ip]ALLOWED_HOSTS 必须配置否则 Django 会拒绝非白名单域名的请求返回 400 错误。把这个配置写好后重启 waitress 和 Nginx博客就正式上线了。6.3 部署前必须做的一次性设置正式部署前还需要做几件容易忘的事用 collectstatic 收集所有静态文件确保 staticfiles 目录完整用python manage.py check --deploy检查部署配置是否有隐患备份好 db.sqlite3 数据库文件或用 Django 的 dumpdata 命令导出数据确认超级管理员账号已创建否则后台无法登录check --deploy这个命令很好用它会列出项目里不符合生产环境要求的配置项。我第一次执行时列出了七八条警告包括 SECRET_KEY 硬编码、CSRF 未配置 HTTPS 等逐项处理后心里踏实很多。7. 常见问题与排查技巧实录最后这部分是我这次项目中实际遇到过的问题也是热搜词里很多人搜的内容。我直接整理成速查表遇到问题对照着查就行。7.1 问题速查表问题现象可能原因解决方法运行项目提示 No module named django忘记激活虚拟环境执行venv\Scripts\activate后再运行makemigrations 后提示 No changes detectedapp 未加入 INSTALLED_APPS检查 settings.py 的 INSTALLED_APPS 里是否包含 blog修改模型字段后 migrate 报错新增字段未设置默认值给字段加 default 参数或允许 blankTrue、nullTrue后台管理页面样式丢失DEBUGFalse 且静态文件未正确配置执行 collectstatic确认 Nginx 的 /static/ 路由配置正确模板里 img 图片显示不了未使用 {% static %} 标签模板顶部加 {% load static %}用 {% static 路径 %} 引用文章详情页报 404slug 不匹配或状态不是 published检查 URL 中的 slug 与数据库记录是否一致分页链接点了没反应分页参数名不对确认视图中使用request.GET.get(page)中文内容显示乱码数据库编码或响应头有问题settings.py 中 LANGUAGE_CODE 设为 zh-hans模板声明 UTF-8waitress 启动后局域网无法访问ALLOWED_HOSTS 未配置或防火墙拦截在 ALLOWED_HOSTS 加入本机 IP放行 8000 和 80 端口collectstatic 提示文件已存在静态文件目录重复用--clear参数强制重建 staticfiles 目录7.2 vscode 里写 img 标签显示不了的详细排查这条热搜词我想展开说。很多人在 VSCode 里写 Django 模板图片路径看着没问题运行后就是加载不出来。我踩过的排查路径是这样的先打开浏览器开发者工具看 Network 面板里图片的请求情况。如果请求 URL 是http://127.0.0.1:8000/images/logo.png而不是/static/images/logo.png说明模板里没用{% static %}标签直接写了相对路径。如果 URL 正确但是状态 404去 static 目录下确认文件是否真的存在文件名大小写是否一致。Windows 文件系统不区分大小写但 Linux 区分本地正常部署到服务器后反而 404很多就是这个原因。如果 URL 正确、文件存在、状态 200但图片还是不显示可能是 MIME 类型问题或者文件本身损坏。用 VSCode 打开图片文件确认不是 0 字节。7.3 Django 执行查询与删除对象常踩的坑热搜词里有一条是“django执行查询-删除对象”。这块新手确实容易踩坑核心就一句话删除对象时Django 默认不会处理级联关系。比如你要删除一个分类category Category.objects.get(slugpython) category.delete()因为 Post 里 category 字段是on_deletemodels.CASCADE这个 delete 操作会级联删除该分类下的所有文章。如果你希望“分类删掉但文章保留”就应该把 on_delete 改成SET_NULL同时让 category 字段允许为空category models.ForeignKey( Category, verbose_name分类, on_deletemodels.SET_NULL, nullTrue, blankTrue )那删除数据后数据库自增 ID 会不会重用Django 默认不会。SQLite 的自增 ID 保证单调递增删除最后一条后新插入的记录也不会复用旧 ID。如果你开过开发服务器创建又删除几篇文章会看到 ID 不是连续的这不影响使用别想着去重置它。7.4 生产环境常见的性能与安全细节部署到正式服务器后有几个配置对博客的稳定性和安全很关键。比如把 SECRET_KEY 从 settings.py 里抽离出来放到环境变量或单独的配置文件中开启 HTTPS 后需要把 CSRF_COOKIE_SECURE 和 SESSION_COOKIE_SECURE 设为 True设置 X_FRAME_OPTIONS 为 DENY 防止点击劫持。性能方面文章列表页建议加缓存。最省事的做法是用 Django 的缓存体系整页缓存from django.views.decorators.cache import cache_page urlpatterns [ path(, cache_page(60 * 5)(views.post_list), namepost_list), ]这样首页每 5 分钟才执行一次数据库查询其余请求直接从缓存返回。对博客这种读多写少的场景效果立竿见影。但注意别给详情页做太长时间的整页缓存否则阅读量统计就不准了——详情页数据是每次请求都会更新的。8. 项目管理与后续扩展的一些心得整个项目做完我最大的体会是Django 的学习曲线并不是“陡峭”而是“平缓但很长”。它不像某些框架那样有个明显的顿悟点而是在你每个环节都稍微需要学一点新东西模型迁移、模板语法、URL 解析、Admin 定制、部署配置……每个环节都不难但串起来就是一个完整工程。如果你照着文章把项目跑通了下一步我建议这样扩展加一个评论功能用 Django 的 Form 或 ModelForm 处理表单提交配合 CSRF 保护这是锻炼表单处理能力的好机会。再往后可以做用户注册登录把作者和文章绑定为以后做多作者博客打基础。如果文章量上来了考虑换 PostgreSQL 数据库再把全文搜索加上。如果在 Windows 上部署的 waitress 方案让你觉得切换 Linux 成本太高可以先保持这个架构毕竟博客系统这种规模的项目waitress 单进程的并发能力完全够用。真要扛更大流量到时候再迁到 Linux Gunicorn Nginx 也不迟Django 项目迁移数据库和服务器环境非常平滑框架层面基本不需要改代码。我个人最后的建议是三个“不要”不要在项目初期过度设计不要跳过虚拟环境直接裸装依赖不要在生产环境开着 DEBUG 调试。这三点每一件我都因为偷懒吃过亏现在都老老实实按规范来。博客系统看着小但它把 Web 开发的各个环节都覆盖到了拿它练手、积累经验性价比非常高。
返回列表