news 2026/9/24 19:01:21

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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/,一眼就能看出内容主题。如果直接用数字 ID,URL 就成了/post/1/,既不直观也不利于分享传播。

字段类型上,正文内容用 TextField,标题用 CharField(max_length=200),发布时间用 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 并创建项目和应用

激活虚拟环境后,安装 Django:

pip install django

如果想指定版本,可以写成pip install django==4.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 migrate

migrate 会把你 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_length=100) slug = models.SlugField('URL标识', unique=True) class Meta: verbose_name = '分类' verbose_name_plural = '分类' def __str__(self): return self.name class Tag(models.Model): name = models.CharField('标签名称', max_length=100) slug = models.SlugField('URL标识', unique=True) 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_length=200) slug = models.SlugField('URL标识', unique=True) author = models.ForeignKey(User, verbose_name='作者', on_delete=models.CASCADE) category = models.ForeignKey(Category, verbose_name='分类', on_delete=models.CASCADE) tags = models.ManyToManyField(Tag, verbose_name='标签', blank=True) content = models.TextField('正文内容') excerpt = models.CharField('摘要', max_length=200, blank=True) status = models.CharField('状态', max_length=10, choices=STATUS_CHOICES, default='draft') views = models.PositiveIntegerField('阅读量', default=0) created_at = models.DateTimeField('创建时间', auto_now_add=True) updated_at = models.DateTimeField('更新时间', auto_now=True) published_at = models.DateTimeField('发布时间', default=timezone.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_delete=models.CASCADE的意思是:如果作者或分类被删除,那对应的文章也一并删除。这是 Django 2.0 之后的强制要求,必须显式声明删除行为,避免数据库里出现“孤儿数据”。auto_now_addauto_now的区别也值得记一下:前者只在创建时写入当前时间,后者每次保存都会更新为当前时间。所以创建时间用 auto_now_add,更新时间用 auto_now,这个组合是 Django 项目里的标准做法。

写完模型后,执行:

python manage.py makemigrations blog python manage.py migrate

makemigrations 生成迁移文件,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_slug=None): queryset = Post.objects.filter(status='published') if category_slug: category = get_object_or_404(Category, slug=category_slug) queryset = queryset.filter(category=category) 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, slug=slug, status='published') 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.py:

from django.urls import path from . import views app_name = 'blog' urlpatterns = [ path('', views.post_list, name='post_list'), path('category/<slug:category_slug>/', views.post_list, name='post_list_by_category'), path('post/<slug:slug>/', views.post_detail, name='post_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 lang="zh-hans"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>{% block title %}我的博客{% endblock %}</title> {% load static %} <link rel="stylesheet" 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> <h2><a 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 class="pagination"> {% 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(status='published') make_published.short_description = '将选中文章标记为已发布'

登录后台地址是 /admin/,你之前创建的超级管理员账号在这里登录。

4. 静态文件处理与图片显示

这部分单独拿出来重点讲,因为太多人第一次写 Django 项目时就栽在这里。热搜词里有一条“vscode写img标签 在django的static文件中显示不了”,这几乎是每个新手的必经坑。

4.1 为什么图片加载不出来

Django 处理静态文件和普通 HTML 页面不一样。如果你直接在模板里写:

<img src="images/logo.png" alt="logo">

浏览器会去找当前路径下的 images 目录,但 Django 默认不会把项目里的静态文件映射到 URL 上。你需要在模板里这么写:

{% load static %} <img src="{% static 'images/logo.png' %}" alt="logo">

{% 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.py

4.3 开发环境与生产环境的差异

开发环境下,Django 自带静态文件服务,你只要把文件放进正确目录、用{% static %}标签引用,runserver 就能正常展示。但生产环境不是这样。生产环境下,Django 官方不建议用 runserver 跑业务,也不负责托管静态文件,静态文件应该交给 Nginx 这类服务器软件处理。

所以部署时需要先执行一条命令:

python manage.py collectstatic

这个命令会把所有 app 和 STATICFILES_DIRS 下的静态文件统一收集到一个目录 STATIC_ROOT 下,然后交给 Nginx 对外提供访问。settings.py 里要加:

STATIC_ROOT = BASE_DIR / 'staticfiles'

如果你 DEBUG=False 但没有执行 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_root=settings.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__icontains=keyword) | Q(excerpt__icontains=keyword) | Q(content__icontains=keyword), status='published' ) 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(status='published').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(status='published')[: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 查询,也就是 N+1 问题。解决办法是在视图查询时用prefetch_related('tags')

posts = Post.objects.filter(status='published').prefetch_related('tags').select_related('category')

select_related 针对外键查询,prefetch_related 针对多对多查询。这两个方法都是让 Django 一次性把关联数据查出来,避免循环中反复访问数据库。这个优化在数据量少时感受不明显,文章超过几十篇后,响应速度的差异就出来了。

6. 部署上线:waitress + nginx 方案

开发环境跑通只是第一步,博客要给别人访问,就得部署上线。传统方案是 Linux 服务器 + Gunicorn + Nginx,但如果你手头是 Windows 服务器,或者只是想在本地局域网里让朋友访问一下,那 waitress + Nginx 的组合更合适。这也是热搜词里反复出现的搭配。

6.1 为什么选 waitress

waitress 是一个纯 Python 实现的 WSGI 服务器,最大的好处是跨平台,Windows 上跑得非常好。而 Gunicorn 虽然性能优秀,但对 Windows 支持很有限,经常出现装不上、跑不起来的情况。如果你在 Windows 上用 Django,waitress 是最靠谱的选择。

安装:

pip install waitress

启动:

waitress-serve --port=8000 blog_project.wsgi:application

这条命令会启动一个生产级 WSGI 服务,监听 8000 端口。到这里,你的博客已经可以通过本机 IP 或域名访问了。但此时静态文件还是没处理的状态,Django 在 DEBUG=False 时不会帮你托管静态文件,所以需要 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 参数或允许 blank=True、null=True
后台管理页面样式丢失DEBUG=False 且静态文件未正确配置执行 collectstatic,确认 Nginx 的 /static/ 路由配置正确
模板里 img 图片显示不了未使用 {% static %} 标签模板顶部加 {% load static %},用 {% static '路径' %} 引用
文章详情页报 404slug 不匹配或状态不是 published检查 URL 中的 slug 与数据库记录是否一致
分页链接点了没反应分页参数名不对确认视图中使用request.GET.get('page')
中文内容显示乱码数据库编码或响应头有问题settings.py 中 LANGUAGE_CODE 设为 zh-hans,模板声明 UTF-8
waitress 启动后局域网无法访问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(slug='python') category.delete()

因为 Post 里 category 字段是on_delete=models.CASCADE,这个 delete 操作会级联删除该分类下的所有文章。如果你希望“分类删掉但文章保留”,就应该把 on_delete 改成SET_NULL,同时让 category 字段允许为空:

category = models.ForeignKey( Category, verbose_name='分类', on_delete=models.SET_NULL, null=True, blank=True )

那删除数据后,数据库自增 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), name='post_list'), ]

这样首页每 5 分钟才执行一次数据库查询,其余请求直接从缓存返回。对博客这种读多写少的场景,效果立竿见影。但注意别给详情页做太长时间的整页缓存,否则阅读量统计就不准了——详情页数据是每次请求都会更新的。

8. 项目管理与后续扩展的一些心得

整个项目做完,我最大的体会是:Django 的学习曲线并不是“陡峭”,而是“平缓但很长”。它不像某些框架那样有个明显的顿悟点,而是在你每个环节都稍微需要学一点新东西:模型迁移、模板语法、URL 解析、Admin 定制、部署配置……每个环节都不难,但串起来就是一个完整工程。

如果你照着文章把项目跑通了,下一步我建议这样扩展:加一个评论功能,用 Django 的 Form 或 ModelForm 处理表单提交,配合 CSRF 保护,这是锻炼表单处理能力的好机会。再往后可以做用户注册登录,把作者和文章绑定,为以后做多作者博客打基础。如果文章量上来了,考虑换 PostgreSQL 数据库,再把全文搜索加上。

如果在 Windows 上部署的 waitress 方案让你觉得切换 Linux 成本太高,可以先保持这个架构,毕竟博客系统这种规模的项目,waitress 单进程的并发能力完全够用。真要扛更大流量,到时候再迁到 Linux + Gunicorn + Nginx 也不迟,Django 项目迁移数据库和服务器环境非常平滑,框架层面基本不需要改代码。

我个人最后的建议是三个“不要”:不要在项目初期过度设计,不要跳过虚拟环境直接裸装依赖,不要在生产环境开着 DEBUG 调试。这三点每一件我都因为偷懒吃过亏,现在都老老实实按规范来。博客系统看着小,但它把 Web 开发的各个环节都覆盖到了,拿它练手、积累经验,性价比非常高。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/24 19:00:59

2026电子工厂MES选型:焊点可溯、料号可证、人机可语

1. 为什么2026年选MES不是“挑软件”&#xff0c;而是重构工厂的生存逻辑2026年电子行业工厂选MES&#xff0c;表面看是采购一个系统&#xff0c;实则是一场静默却致命的生存能力重置。我跑过深圳、东莞、苏州、成都四地37家电子代工厂和IDM企业&#xff0c;从年产值8000万的SM…

作者头像 李华
网站建设 2026/9/24 19:00:59

Maven核心机制详解:依赖管理与生命周期实战指南

1. 构建工具为什么绕不开Maven&#xff1a;从三个真实痛点说起 聊到Java后端开发&#xff0c;Maven是个绕不开的家伙。很多刚入行的朋友一开始接触Maven&#xff0c;就是在IDE里点了几个按钮&#xff0c;发现项目能跑起来&#xff0c;然后就没管了。等到项目变大、模块变多&…

作者头像 李华
网站建设 2026/9/24 19:00:11

C#上位机断线排查:用Serilog+ELK打造结构化日志分析方案

做工业上位机这几年&#xff0c;我最怕的从来不是代码编译不过&#xff0c;而是半夜被客户电话叫醒&#xff1a;"设备又断线了"。不是完全断开&#xff0c;也不是完全连不上&#xff0c;就是你越盯着它越正常、你一转身它必定出问题的那种"幽灵断线"。这种…

作者头像 李华
网站建设 2026/9/24 18:59:38

联邦学习实战指南:三数据集算法对比与避坑实践

简介&#xff1a;一份基于Python的联邦学习实验项目&#xff0c;面向人工智能、计算机及相关专业的学生、老师和开发者&#xff0c;既可作为课程设计、毕业设计的参考&#xff0c;也适合入门者理解联邦学习核心算法。项目包含三个递进实验&#xff1a;在Cifar-10上对比FedAvg、…

作者头像 李华
网站建设 2026/9/24 18:58:39

仿真环境接入AI智能体:从API文档到Tool配置的实战指南

上个月我接到一个需求&#xff1a;把团队内部一直在跑的一个仿真环境&#xff08;内部代号就叫 Sim&#xff09;接入 AI 智能体&#xff0c;让模型可以直接通过自然语言调用 Sim 做场景验证。听起来不复杂&#xff0c;但真正动手才发现&#xff0c;从一份 API 文档到一段能用的…

作者头像 李华