news 2026/9/10 17:14:30

Django全栈开发入门:从零构建博客系统实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Django全栈开发入门:从零构建博客系统实战指南

你是不是也想做一个自己的博客系统,却不知道怎么选技术栈?我的建议一直很直接:想入门全栈开发,就从 Django 动手。博客这个项目麻雀虽小、五脏俱全,从数据库建模、ORM 查询到路由、模板渲染、后台管理、文件上传、部署上线,几乎把 Django 全栈开发的核心链路完整走了一遍,做完之后你会对整个 Web 开发流程有一个扎实的体感。

这篇文章围绕“Django全栈开发入门:构建一个博客系统”展开,适合刚看完 Python 基础、想跳出教程自己动手的人,也适合已经会用 Flask 这类轻量框架、但想体验全家桶框架完整度的朋友。我会把从零搭一个博客的完整思路、代码、配置、部署和踩坑过程都整理出来,尽量让你跟着就能复现。

1. 先拆解项目:博客系统的范围与技术选型

1.1 为什么是 Django,而不是其他框架

市面上 Python Web 框架不少,Flask 灵活、FastAPI 性能好、Django 则是一套全家桶。很多人纠结怎么选,我的建议是:如果想系统性地理解全栈开发,Django 是最合适的入门工具。

原因很简单:Django 自带 Admin 后台、ORM、表单处理、认证系统、静态文件管理、模板引擎,这些全是 Web 开发的高频组件。用 Flask 时你可能还要自己去拼 SQLAlchemy、Jinja2、Flask-Login、Flask-Admin,光选型就消耗不少精力;Django 帮你把这些都打通了,你只需要关注业务本身。

而且 Django 的 ORM 设计得很好,Model 层写好后,迁移、建表、查询、关联、删除这些操作都有清晰的规范。对于入门者来说,这种“官方给你标准答案”的体验非常友好,你能把精力放在理解业务逻辑上,而不是纠结某个库怎么配置。

我用的是 Django 4.2 LTS 版本。为什么不追最新版?LTS(Long Term Support)意味着长期维护,社区资料多、坑少,尤其在生产环境,稳定比“新”重要得多。Python 版本我推荐 3.10 以上,4.2 对 3.10/3.11 的支持都很成熟。

1.2 博客系统的功能范围怎么定

很多新手做项目时容易犯一个错误:功能越加越多,最后烂尾。我踩过这个坑,所以这次做博客,我一开始就明确边界,只做核心闭环:

  • 文章的发布、编辑、删除(通过 Admin 后台管理,不单独做前端管理页面)
  • 文章分类与标签
  • 首页文章列表 + 分页
  • 文章详情页
  • 图片上传与视频上传
  • 部署上线

评论区、用户注册、点赞这些先不做,不是难,而是它们会分散你对主线的注意力。入门项目的核心目标是跑通“数据模型 → 视图 → 模板 → 部署”这条链路,而不是功能越多越好。

等到这个基础版本跑通了,再往上加功能就很容易。Django 项目不像单体脚本,它天然是模块化的,加一个应用(app)或加一张表,不会伤筋动骨。

1.3 环境准备与虚拟环境管理

开发环境我建议直接在本地 Linux/macOS 上做,Windows 也能做,只是个别命令需要调整。这里我把虚拟环境和项目初始化一起讲了。

先用 venv 创建虚拟环境,这是 Django 项目的第一步。虚拟环境的作用是隔离项目的 Python 依赖,不同项目之间不会互相污染,特别是你同时在维护两个 Django 项目时,一个要 Django 3,一个要 Django 4,不隔离就会乱套。

mkdir django-blog cd django-blog python3 -m venv venv source venv/bin/activate # Windows 下是 venv\Scripts\activate

虚拟环境激活后,命令行前面会出现(venv)标记,这时再安装依赖:

pip install django pillow

pillow是 Python 处理图片的库,Django 的ImageField依赖它,不安装的话,后面上传图片功能会直接报错。

创建项目和 App:

django-admin startproject blogproject . python manage.py startapp blog

这里我在项目名后面加了一个点.,意思是把manage.py放在当前目录,而不是再套一层目录。很多教程没写这个点,导致后面目录结构多一层,很容易懵。startapp blog则是创建博客这个应用,Django 里“项目”(project)和“应用”(app)是两个概念:项目是整个网站,应用是其中的功能模块。一个项目里可以有多个应用,比如bloguserscomments,每个应用各管一摊。

然后记得把blog加到settings.pyINSTALLED_APPS里,否则 Django 不认识这个应用。

INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', # ... 'blog', ]

2. 数据库与核心模型设计

2.1 文章、分类、标签三个核心模型

博客的数据模型不复杂,但设计得好不好,直接影响后续开发的体验。我的设计是这样的:

  • Category(分类):分类是层级简单的对象,每个分类有名称和别名。
  • Tag(标签):标签和文章是多对多关系。
  • Post(文章):文章包含标题、正文、封面图、视频、所属分类、标签、发布时间、更新时间等。

写代码时,我建议字段尽量一次想清楚,但要留好扩展的余地。比如状态字段,我先用draftpublished两种状态,后续如果要加“置顶”“审核中”,只需要加枚举值,不需要改表结构。

以下是blog/models.py的核心代码:

from django.db import models from django.urls import reverse from django.utils import timezone class Category(models.Model): name = models.CharField('分类名称', max_length=50) slug = models.SlugField('别名', unique=True) class Meta: verbose_name = '分类' verbose_name_plural = verbose_name def __str__(self): return self.name class Tag(models.Model): name = models.CharField('标签名称', max_length=50) slug = models.SlugField('别名', unique=True) class Meta: verbose_name = '标签' verbose_name_plural = verbose_name def __str__(self): return self.name class Post(models.Model): STATUS_CHOICES = ( ('draft', '草稿'), ('published', '已发布'), ) title = models.CharField('标题', max_length=200) slug = models.SlugField('别名', unique=True) category = models.ForeignKey(Category, verbose_name='分类', on_delete=models.PROTECT, related_name='posts') tags = models.ManyToManyField(Tag, verbose_name='标签', blank=True, related_name='posts') cover = models.ImageField('封面图', upload_to='cover/%Y/%m/%d/', blank=True, null=True) video = models.FileField('视频', upload_to='video/%Y/%m/%d/', blank=True, null=True) body = models.TextField('正文') status = models.CharField('状态', max_length=20, choices=STATUS_CHOICES, default='draft') created_at = models.DateTimeField('创建时间', default=timezone.now) updated_at = models.DateTimeField('更新时间', auto_now=True) class Meta: verbose_name = '文章' verbose_name_plural = verbose_name ordering = ('-created_at',) def __str__(self): return self.title def get_absolute_url(self): return reverse('blog:post_detail', kwargs={'slug': self.slug})

这里有几个关键点需要展开说。

on_delete=models.PROTECT是特意选的。分类如果被文章引用,PROTECT会阻止删除分类并抛出异常,避免误删导致整个 blog 文章的分类变成空指针。新手经常用CASCADE,但博客场景下分类删除会连带删除所有文章,这通常不是你想要的结果。如果你确定某个分类删掉后文章也一起删,那用CASCADE没错,但博客场景更合理的是PROTECTSET_NULL

related_name也很重要。设置了related_name='posts'后,你可以通过category.posts.all()拿到这个分类下的所有文章,反向查询语义清晰。如果不设置,Django 默认生成category.post_set.all(),能看但不够直观。

ImageFieldFileFieldupload_to我做了日期路径,这样上传的文件会按cover/2025/06/18/xxx.jpg组织,避免一个目录下文件太多。

2.2 图片上传与视频上传的字段设计

最初设计时,图片和视频我合并成了一个通用FileField,后来发现不行。ImageField会自动校验文件是不是合法图片,并在 Admin 后台生成预览缩略图,这对写博客体验很重要。所以封面图用ImageField,视频单独用FileField

两个字段我都设置了blank=True, null=True,表示封面图和视频不是必填项。注意blank是表单层面的校验,null是数据库层面的约束,两者一起用才是“可以不填”。

视频上传比图片麻烦的地方在于体积和格式。图片一般压缩后几百 KB 到几 MB,视频动辄几十 MB 甚至更大。开发环境runserver无所谓,但生产环境如果你用的是 Nginx,通常还要配一个客户端请求大小的限制,比如client_max_body_size 100m;,否则上传大视频会直接被挡在 Nginx 层,Django 根本收不到请求,这个问题我到部署章节再细说。

2.3 数据迁移与 ORM 查询/删除对象

模型写好后,执行迁移:

python manage.py makemigrations blog python manage.py migrate

makemigrations是生成迁移文件,不直接改数据库;migrate才是把迁移执行到数据库。这两步很多人搞混,记住:makemigrations是“准备变更”,migrate是“应用变更”。

写好模型后进入 Django Shell 测试一下 ORM 操作,这是个很好的习惯,能帮你及时发现问题:

python manage.py shell

在 shell 里可以做这些操作:

from blog.models import Category, Tag, Post from django.utils import timezone # 创建分类和标签 cat = Category.objects.create(name='Python', slug='python') tag = Tag.objects.create(name='Django', slug='django') # 创建文章 post = Post.objects.create( title='我的第一篇 Django 博客', slug='my-first-django-post', category=cat, body='正文内容……', status='published', ) # 关联标签 post.tags.add(tag) # 查询 Post.objects.filter(status='published') Post.objects.filter(category__name='Python') post = Post.objects.get(slug='my-first-django-post') # 删除对象 post.delete()

这里我要重点说说“删除对象”这个热词。ORM 删除有几种方式,区别很大。

  • obj.delete():删除单个对象,返回(总删除数, 明细字典),它会把关联的对象按规则处理。
  • QuerySet.delete():批量删除,比如Post.objects.filter(category=cat).delete()会删除所有匹配的文章。
  • Category.objects.get(slug='python').delete():由于我们用了PROTECT,如果分类下有文章,这一步会直接抛出ProtectedError

我在实际项目中遇到过一个问题:批量删除时没看返回值,以为删干净了,结果关联表里还有残留数据。所以删除操作一定要先确认归属关系,尤其是有外键和多对多的模型。

3. 视图、路由与模板三件套

3.1 首页与详情页视图逻辑

入门阶段,我建议先用函数视图(FBV)理解流程,跑通后再升级到类视图(CBV)。这里我先展示 FBV 写法,后面也会给出 CBV 的版本作为升级参考。

blog/views.py

from django.shortcuts import render, get_object_or_404 from django.core.paginator import Paginator from .models import Post def post_list(request): posts = Post.objects.filter(status='published') paginator = Paginator(posts, 5) page_number = request.GET.get('page') page_obj = paginator.get_page(page_number) return render(request, 'blog/post_list.html', {'page_obj': page_obj}) def post_detail(request, slug): post = get_object_or_404(Post, slug=slug, status='published') return render(request, 'blog/post_detail.html', {'post': post})

get_object_or_404是 Django 的快捷方式:如果查询结果不存在,直接返回 404 页面,不用手动写 try/except。

分页用的是Paginator,参数 5 表示每页 5 篇文章。在模板里可以展示上一页/下一页以及页码列表,这个后面会讲。

升级后的 CBV 版本长这样:

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 = 'page_obj' paginate_by = 5 def get_queryset(self): return super().get_queryset().filter(status='published') class PostDetailView(DetailView): model = Post template_name = 'blog/post_detail.html' slug_url_kwarg = 'slug'

CBV 代码更少,但新手看它会有一种“魔法感”——你得知道ListView默认去查什么、模板里默认上下文变量叫什么。我建议先用 FBV 写一遍,再看 CBV 就会豁然开朗。

3.2 URL 设计与命名空间

blog/urls.py里定义路由:

from django.urls import path from . import views app_name = 'blog' urlpatterns = [ path('', views.post_list, name='post_list'), path('post/<slug:slug>/', views.post_detail, name='post_detail'), ]

然后在项目的blogproject/urls.py里包含进来:

from django.contrib import admin from django.urls import path, include urlpatterns = [ path('admin/', admin.site.urls), path('', include('blog.urls')), ]

注意两个细节:第一,app_name = 'blog'定义了命名空间,这样在模板中可以使用{% url 'blog:post_detail' slug=post.slug %},即使将来改了 URL 路径,模板代码不需要变动;第二,<slug:slug>是 URL 转换器,限制参数格式为字母、数字、连字符、下划线,避免非法字符。

之前我看有人把所有业务路由都写在项目根urls.py里,应用一多就变得极难维护。Django 的哲学是“应用自治”,每个 app 有自己的urls.py,根urls.py只管include,这个习惯越早养成越好。

3.3 模板继承与页面渲染

模板是 Django 里很容易被忽略但很重要的部分。我并没有直接用 Django Admin 的界面作为前台,而是自己写了一套模板,原因是我们需要理解模板渲染的机制。

先创建一个基础模板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> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> </head> <body> <nav class="navbar navbar-expand-lg navbar-light bg-light"> <div class="container"> <a class="navbar-brand" href="/">我的博客</a> </div> </nav> <div class="container mt-4"> {% block content %} {% endblock %} </div> </body> </html>

然后templates/blog/post_list.html

{% extends 'base.html' %} {% block title %}首页 - 我的博客{% endblock %} {% block content %} <h1>文章列表</h1> {% for post in page_obj %} <article class="mb-4"> <h2> <a href="{% url 'blog:post_detail' slug=post.slug %}">{{ post.title }}</a> </h2> <p class="text-muted">{{ post.created_at|date:"Y-m-d" }} | {{ post.category.name }}</p> {% if post.cover %} <img src="{{ post.cover.url }}" alt="{{ post.title }}" class="img-fluid" style="max-width: 300px;"> {% endif %} <p>{{ post.body|truncatechars:100 }}</p> </article> {% empty %} <p>还没有文章,去后台发布一篇吧。</p> {% endfor %} <nav aria-label="Page navigation"> <ul class="pagination"> {% if page_obj.has_previous %} <li class="page-item"><a class="page-link" href="?page={{ page_obj.previous_page_number }}">上一页</a></li> {% endif %} <li class="page-item disabled"><span class="page-link">第 {{ page_obj.number }} / {{ page_obj.paginator.num_pages }} 页</span></li> {% if page_obj.has_next %} <li class="page-item"><a class="page-link" href="?page={{ page_obj.next_page_number }}">下一页</a></li> {% endif %} </ul> </nav> {% endblock %}

templates/blog/post_detail.html

{% extends 'base.html' %} {% block title %}{{ post.title }} - 我的博客{% endblock %} {% block content %} <article> <h1>{{ post.title }}</h1> <p class="text-muted">{{ post.created_at|date:"Y-m-d H:i" }} | {{ post.category.name }}</p> {% if post.cover %} <img src="{{ post.cover.url }}" alt="{{ post.title }}" class="img-fluid"> {% endif %} <div class="mt-4"> {{ post.body|linebreaks }} </div> {% if post.video %} <video controls class="w-100 mt-3"> <source src="{{ post.video.url }}" type="video/mp4"> 你的浏览器不支持 video 标签。 </video> {% endif %} <p class="mt-3"> 标签: {% for tag in post.tags.all %} <span class="badge bg-secondary">{{ tag.name }}</span> {% endfor %} </p> </article> <p><a href="{% url 'blog:post_list' %}">返回列表</a></p> {% endblock %}

模板继承机制很容易理解:base.html是框架,block是插槽,子模板通过extends继承框架并填充block。这比在每一个页面里复制导航栏、页脚代码要优雅得多。

还有几个模板过滤器值得记住:|date:"Y-m-d"格式化日期,|truncatechars:100截断文本,|linebreaks把换行转成<br><p>,这些都是在实际写作中非常高频的功能。

4. Admin 后台与媒体文件管理

4.1 注册模型与后台增强

Django Admin 是这套框架最大的卖点之一。写博客时,我不需要单独做一套后台 UI,直接用 Admin 就能管文章、分类、标签。

blog/admin.py注册模型:

from django.contrib import admin from .models import Category, Tag, Post @admin.register(Category) class CategoryAdmin(admin.ModelAdmin): list_display = ('name', 'slug') prepopulated_fields = {'slug': ('name',)} @admin.register(Tag) class TagAdmin(admin.ModelAdmin): list_display = ('name', 'slug') prepopulated_fields = {'slug': ('name',)} @admin.register(Post) class PostAdmin(admin.ModelAdmin): list_display = ('title', 'category', 'status', 'created_at', 'updated_at') list_filter = ('status', 'category', 'tags') search_fields = ('title', 'body') prepopulated_fields = {'slug': ('title',)} filter_horizontal = ('tags',) date_hierarchy = 'created_at' list_editable = ('status',)

prepopulated_fields的作用是:在 Admin 页面输入标题时,slug 字段会自动根据标题生成,省去手动输入的麻烦。注意中文标题生成 slug 会变成空串,所以实际发布中文文章时,还是需要手动填一下 slug,推荐用拼音或者英文字段。

filter_horizontal让多对多字段的标签选择变成左右双栏选择,体验比默认的下拉框好得多。list_editable让你可以直接在列表页修改状态,不用点进详情,非常方便。

创建超级管理员:

python manage.py createsuperuser

然后启动开发服务器:

python manage.py runserver

访问http://127.0.0.1:8000/admin/登录,就可以在后台发布文章了。

4.2 静态文件与媒体文件配置

刚入门时,很多人会把静态文件(CSS、JS、图片)和媒体文件(用户上传的内容)混在一起,其实它们的处理逻辑完全不同。

静态文件是项目自带的资源,用STATICFILES_DIRS配置收集目录;媒体文件是运行时动态上传的,用MEDIA_URLMEDIA_ROOT配置。

settings.py里我建议这样配置:

import os # 静态文件 STATIC_URL = '/static/' STATICFILES_DIRS = [BASE_DIR / 'static'] STATIC_ROOT = BASE_DIR / 'staticfiles' # 媒体文件 MEDIA_URL = '/media/' MEDIA_ROOT = BASE_DIR / 'media'

STATIC_ROOT是生产环境执行collectstatic后所有静态文件汇总的目录,这是给 Nginx 用的,开发环境不需要。如果没设STATIC_ROOT,部署时执行collectstatic会报错。

开发环境要能正常访问媒体文件,还需要在根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)

这段代码只在 DEBUG 模式下生效,生产环境媒体文件由 Nginx 直接服务,不需要 Django 来处理,否则性能会非常差。

4.3 富文本/Markdown 方案

默认的TextField在 Admin 后台就是一个文本框,写文章体验比较原始。我试过几种方案:CKEditor、TinyMCE、django-markdownx,最后选择了django-markdownx,原因是它和 Admin 的集成最顺滑,支持实时预览。

安装配置:

pip install django-markdownx

settings.py添加:

INSTALLED_APPS = [ # ... 'markdownx', ]

urls.py添加:

urlpatterns += [ path('markdownx/', include('markdownx.urls')), ]

然后把模型里的body字段改成:

from markdownx.models import MarkdownxField body = MarkdownxField('正文')

这样 Admin 里就会出现带预览的编辑框,写 Markdown 时右侧实时渲染 HTML,非常香。

不过 Markdown 渲染后,前面的post.body|linebreaks就不能直接用了,需要改成安全输出。为了简单,我建议在post_detail.html里把正文渲染成 HTML 后手动用|safe过滤器,或者干脆写一个自定义模板过滤器把 Markdown 转成 HTML。这里给你一个简单的自定义过滤器示例:

blog/templatetags/下创建markdown_extras.py

import markdown from django import template from django.utils.safestring import mark_safe register = template.Library() @register.filter(name='markdown') def render_markdown(text): md = markdown.Markdown(extensions=['extra', 'codehilite']) return mark_safe(md.convert(text))

然后在模板里:

{% load markdown_extras %} <div class="mt-4"> {{ post.body|markdown }} </div>

用 Markdown 写博客的好处很多:格式干净、不依赖编辑器、版本管理友好。缺点是没有图形界面的排版按钮,但博客写作场景完全够用。

5. 部署上线与常见问题排查

5.1 Linux 服务器部署:宝塔环境为例

本地开发跑通了,距离“全栈”还差最后一步:部署。很多新手死在部署这一步,其实部署本身不难,难的是理解其中的流程。

我先说自己踩过的一个大坑:直接在生产环境用runserver。Django 官方文档说得非常清楚,runserver只适合开发调试,性能差且不安全,生产环境必须用 WSGI 服务器(Gunicorn 或 uWSGI)来运行 Django 应用,再用 Nginx 反向代理。

这里以宝塔面板 + Gunicorn + Nginx 为例,因为宝塔的 Python 项目管理器可以简化不少操作。

安装 Gunicorn:

pip install gunicorn

在项目根目录执行以下命令,验证 Gunicorn 能正常启动:

gunicorn blogproject.wsgi:application --bind 0.0.0.0:8000

blogproject.wsgi:application是 WSGI 入口,0.0.0.0:8000表示监听所有网卡的 8000 端口。如果这一步能跑通,说明项目基本可以上线了。

然后是在 Nginx 中配置反向代理。核心配置如下:

server { listen 80; server_name your_domain.com; client_max_body_size 100m; location /static/ { alias /www/wwwroot/django-blog/staticfiles/; } location /media/ { alias /www/wwwroot/django-blog/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; } }

部署前先执行:

python manage.py collectstatic

这个命令会把所有静态文件统一复制到STATIC_ROOT指定的目录,Nginx 的alias指向这里。如果不执行collectstatic,你会看到一个只有 HTML 没有样式的页面,甚至控制台报 404。

生产环境里settings.py还需要修改几个关键项:

DEBUG = False ALLOWED_HOSTS = ['your_domain.com', 'your_server_ip']

DEBUG = False之后,Django 不再输出详细的错误页面,而是显示一个通用 500 页面。如果你需要排查错误,可以看日志,或者临时把DEBUG打开。

5.2 高频报错与排查实录

我整理了一张速查表,基本覆盖了 Django 博客项目从开发到部署最常见的报错:

报错信息原因解决方案
ModuleNotFoundError: No module named 'PIL'没装 Pillowpip install pillow
No migrations to apply迁移文件已是最新,或数据库表已存在检查migrations目录,必要时python manage.py migrate --fake
TemplateDoesNotExist模板路径不对,或APP_DIRS未开检查TEMPLATES配置和templates目录位置
Invalid HTTP_HOST headerALLOWED_HOSTS没配置ALLOWED_HOSTS中加入域名或 IP
Bad Request (400)ALLOWED_HOSTS配置不正确同上
DisallowedHost请求的 Host 不在ALLOWED_HOSTS同上
SyntaxError: (unicode error)Python 版本或编码问题使用 UTF-8 编码,检查 Python 版本
OperationalError: no such column模型字段改动后没迁移python manage.py makemigrations && python manage.py migrate
AttributeError: 'NoneType' object has no attribute 'split'SECRET_KEY读取失败或环境变量缺失检查环境变量
UnicodeDecodeError文件编码问题在 Linux 上设置export LC_ALL=C.UTF-8
413 Request Entity Too Large上传文件超过 Nginx 限制设置client_max_body_size
502 Bad GatewayGunicorn 没启动或端口不对检查 Gunicorn 进程与 Nginx proxy_pass 是否一致

这里我单独说一个最常见的坑:python manage.py migratedjango.db.utils.ProgrammingError: relation "blog_post" already exists。这种情况通常是你之前migrate过一次,后来又手动删了数据库,但迁移表的记录还在,或者你手动删了表但迁移记录还在。

解决方法是进django_migrations表看一下记录,确认哪些迁移已经被记录。如果确实是自己手动改动了数据库结构,最简单的办法是备份数据后,把blog应用的所有迁移文件删掉重新生成,再执行migrate。注意千万别在生产环境随便删迁移,删了之后恢复数据很麻烦。

5.3 安全与性能注意事项

入门项目上线后,有几个安全项必须检查:

  • SECRET_KEY不要硬编码在代码里,尤其别推到公开仓库。我自己遇到过密钥泄露导致 CSRF 校验失败的教训。
  • 数据库不要用默认的 SQLite 跑生产环境。SQLite 在低并发场景还能用,但换到 MySQL 或 PostgreSQL 才是长久的方案。Django 的 ORM 屏蔽了大部分差异,切换代价不大。
  • 上传文件一定要限制类型和大小。虽然 Django 的 ImageField 会校验图片格式,但恶意文件仍然可能通过 FileField 上传。安全起见,可以在视图或表单层加一层扩展名校验。

性能方面,博客项目在初期不会遇到太大瓶颈,但有一点值得提前做:给常用查询加select_relatedprefetch_related,避免 N+1 查询。比如文章列表页如果每篇文章都显示分类名,默认会为每篇文章单独查询一次分类表,文章多了性能就低了。优化方式是:

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

对于多对多的 tags,用prefetch_related('tags')。我在做列表页时加了这个优化后,100 篇文章的页面查询从 100+ 次 SQL 降到了 3 次,效果非常明显。

6. 后续扩展方向

6.1 从 Django REST Framework 走向前后端分离

如果你把博客的 HTML 模板版本做完了,下一步我建议学 Django REST Framework(DRF),把同一个博客改造成 API 服务。前后端分离是目前全栈开发的主流形态,后端只提供 JSON 接口,前端用 Vue 或 React 单独开发。

DRF 的使用思路和 Django 一脉相承。你不需要重新设计数据库,只需要为现有的Post模型写序列化器:

from rest_framework import serializers from .models import Post class PostSerializer(serializers.ModelSerializer): class Meta: model = Post fields = ['id', 'title', 'slug', 'category', 'tags', 'created_at']

然后写一个视图集和路由,就能对外暴露 API 了。这种渐进式扩展的好处是:你的数据模型不用推翻重来,只需加一层接口层。

6.2 Django + Vue 整合实操

“Django 和 Vue 整合”是热词,这个话题看起来复杂,其实核心就一句话:Vue 负责前端页面,Django 负责 API。

实际项目里有两种常见做法。第一种是前后端完全分离,Django 部署为 API 服务,Vue 打包后的静态文件放在 Nginx 里,两者通过/api/路径通信。第二种是混合开发,把 Vue 打包后放到 Django 的static目录中,由 Django 直接托管前端页面,适合小项目部署。

我做过第二种方案,体验不错。具体步骤是:用 Vite 构建 Vue 项目,npm run build生成dist目录,然后把dist下的内容复制到 Django 的static/vue/目录,再在 Django 中写一个视图渲染index.html。这样部署时只需要跑一个 Django 服务,省心不少。

6.3 继续补充的全栈技能点

如果你打算以全栈开发为目标,做完了这个博客后,还可以继续挑战以下技能点:

  • 给文章加评论功能:学习 Django 的FormModelForm、CSRF 防护和用户认证。
  • 加一个搜索功能:可以先在 Django 里用icontains做最简单的模糊搜索,再了解 Elasticsearch 这类搜索引擎的接入方式。
  • 加一个 RSS 订阅:Django 内置了django.contrib.syndication,几行代码就能给博客加上 RSS 输出。
  • 了解 Docker 部署:把 Django、Nginx、MySQL 全部打包成容器,这是目前企业里很常见的交付方式。

这个博客项目只是起点,但它把全栈开发的骨架搭了出来:前端页面、后端接口、数据库模型、文件上传、部署上线,你都已经走了一遍。后面每一个扩展方向,都是在往这个骨架上添加更专业的血肉。按照我个人的经验,把一个简单的项目完整走完,比跟着教程敲十遍代码要有效得多。希望这篇实战笔记能帮你少踩一些我踩过的坑。

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

Arbress数据处理工具:智能表格清洗与分析实战指南

1. Arbress工具概述与核心价值 Arbress作为一款新兴的数据处理工具&#xff0c;在自动化办公领域逐渐崭露头角。它通过智能算法实现表格数据的快速清洗、转换与分析&#xff0c;特别适合需要处理大量结构化数据的财务、运营和科研人员。我在实际使用中发现&#xff0c;相比传统…

作者头像 李华
网站建设 2026/9/10 17:09:30

深入PHP底层:Zend引擎执行流程与性能优化实战

干了这么多年PHP&#xff0c;接手的项目从几百行的小脚本到几百万行的老古董都有&#xff0c;要说最值钱的经验&#xff0c;还真不是背过多少函数&#xff0c;而是搞明白PHP和Zend引擎之间那点“房客与房东”的关系。很多人写出来的代码能跑&#xff0c;但线上一压测就崩&#…

作者头像 李华
网站建设 2026/9/10 17:08:06

图数据结构:核心概念、技术栈与工业实践

1. 图&#xff08;Graph&#xff09;基础概念与核心价值图这种数据结构在计算机科学领域已经存在超过半个世纪&#xff0c;但直到最近十年才真正迎来爆发式应用。作为一名处理过数十个图相关项目的工程师&#xff0c;我亲眼见证了图从学术论文走向工业界落地的全过程。图本质上…

作者头像 李华
网站建设 2026/9/10 17:07:28

虚拟电厂多时间尺度调度中的储能容量衰减建模与Matlab复现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/10 17:04:53

软考系统架构设计师备考:231道精选真题刷题策略全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/10 17:04:38

Flutter在OpenHarmony中实现Tour功能的实践指南

1. 项目概述&#xff1a;Flutter在OpenHarmony中的Tour功能实现在跨平台开发领域&#xff0c;Flutter与OpenHarmony的结合正逐渐成为开发者关注的新方向。这次我们要探讨的是如何在OpenHarmony环境下使用Flutter实现页面引导&#xff08;Tour&#xff09;功能——这种常见于新用…

作者头像 李华