IT策士 10余年一线大厂经验,专注 IT 思维、架构、职场进阶。我会在公众号、今日头条持续发布最新文章,助你少走弯路。
从这篇文章开始Django 实战教程,我们将从零开始,一步步构建一个功能完整的博客系统,包含文章、分类和标签。整个过程会提供大量可直接运行的代码示例与控制台输出,帮助新手理解每一步在做什么,也为进阶开发者梳理最佳实践。
1. 为什么选择 Django?
Django 是一个高级 Python Web 框架,它鼓励快速开发和清晰、务实的设计。用 Django 搭建一个博客,你能在极短时间内获得包含后台管理、文章发布、分类标签筛选等功能的完整站点。它自带的管理后台更是让内容管理变得零成本。
本文将带你从环境搭建到功能上线,逐步实现:
-
文章(Post)的发布、列表与详情展示
-
分类(Category)与标签(Tag)的管理和多对多关联
-
利用 Django Admin 高效管理内容
-
视图、模板与 URL 的完整对接
-
进阶实践:分页、Markdown 渲染与搜索
所有步骤都配有真实的控制台输出,让你感受"所见即所得"的开发体验。
2. 环境准备与项目创建
2.1 安装 Python 与虚拟环境
确保已安装 Python 3.8+。接着创建并激活虚拟环境:
bash
# 创建项目目录
mkdir myblog
cd myblog
# 创建虚拟环境
python -m venv venv
# 激活虚拟环境(Linux/macOS)
source venv/bin/activate
# 或 Windows
venv\Scripts\activate
控制台会显示环境名称:
bash
(venv) user@host:~/myblog$
2.2 安装 Django 并创建项目
bash
pip install django
django-admin startproject config .
注意最后的 . 表示在当前目录创建项目,避免多余嵌套。执行后目录结构如下:
bash
myblog/
├── config/
│ ├── __init__.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── manage.py
└── venv/
验证安装:
bash
python manage.py --version
# 输出:4.2.5 (示例版本)
2.3 创建博客应用
Django 项目由多个"应用"组成,我们将博客功能独立为一个应用:
bash
python manage.py startapp blog
现在项目结构变为:
bash
myblog/
├── blog/
│ ├── migrations/
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── models.py
│ ├── tests.py
│ └── views.py
├── config/
├── manage.py
└── venv/
注册应用 :打开 config/settings.py,找到 INSTALLED_APPS,加入 'blog':
bash
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'blog', # 我们的博客应用
]
3. 定义数据模型:文章、分类、标签
博客的核心是数据。我们在 blog/models.py 中定义三个模型,并理清它们的关系:一篇文章属于一个分类(ForeignKey),可以拥有多个标签(ManyToManyField)。
bash
from django.db import models
from django.urls import reverse
from django.utils.text import slugify
class Category(models.Model):
name = models.CharField(max_length=100, unique=True)
slug = models.SlugField(max_length=100, unique=True)
description = models.TextField(blank=True)
class Meta:
verbose_name = '分类'
verbose_name_plural = '分类'
ordering = ['name']
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse('blog:category_detail', args=[self.slug])
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.name)
super().save(*args, **kwargs)
class Tag(models.Model):
name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(max_length=50, unique=True)
class Meta:
verbose_name = '标签'
verbose_name_plural = '标签'
ordering = ['name']
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse('blog:tag_detail', args=[self.slug])
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.name)
super().save(*args, **kwargs)
class Post(models.Model):
STATUS_CHOICES = (
('draft', '草稿'),
('published', '已发布'),
)
title = models.CharField(max_length=200)
slug = models.SlugField(max_length=200, unique_for_date='created')
author = models.ForeignKey(
'auth.User',
on_delete=models.CASCADE,
related_name='blog_posts'
)
body = models.TextField()
category = models.ForeignKey(
Category,
on_delete=models.SET_NULL,
null=True,
related_name='posts'
)
tags = models.ManyToManyField(Tag, blank=True, related_name='posts')
status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='draft')
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['-created']
verbose_name = '文章'
verbose_name_plural = '文章'
indexes = [
models.Index(fields=['-created']),
]
def __str__(self):
return self.title
def get_absolute_url(self):
return reverse('blog:post_detail', args=[self.created.year,
self.created.month,
self.created.day,
self.slug])
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.title)
super().save(*args, **kwargs)
模型设计解释:
-
Category和Tag都有slug字段,用于生成对 SEO 友好的 URL。 -
Post.category是外键,删除分类时文章不会被删除,而是将其分类设为NULL(SET_NULL)。 -
Post.tags是多对多,一篇文章可以有多个标签,一个标签也能对应多篇文章。 -
unique_for_date保证同一天内 slug 唯一,避免 URL 冲突。 -
get_absolute_url()定义了访问每个对象详情页的规范路径,后续在模板和管理后台中都会用到。
3.1 生成并执行迁移
bash
python manage.py makemigrations blog
python manage.py migrate
控制台输出示例:
bash
Migrations for 'blog':
blog/migrations/0001_initial.py
- Create model Category
- Create model Tag
- Create model Post
- Add field tags to post
Operations to perform:
Apply all migrations: admin, auth, blog, contenttypes, sessions
Running migrations:
Applying blog.0001_initial... OK
现在 SQLite 数据库文件 db.sqlite3 已生成,我们的表结构已就绪。
4. 玩转 Django 管理后台
4.1 创建超级用户
bash
python manage.py createsuperuser
按提示输入用户名、邮箱和密码(密码输入时没有回显):
bash
用户名: admin
电子邮件地址: admin@example.com
Password: ********
Password (again): ********
Superuser created successfully.
4.2 将模型注册到 Admin
编辑 blog/admin.py:
bash
from django.contrib import admin
from .models import Category, Tag, Post
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
list_display = ('name', 'slug', 'description')
prepopulated_fields = {'slug': ('name',)}
search_fields = ('name',)
@admin.register(Tag)
class TagAdmin(admin.ModelAdmin):
list_display = ('name', 'slug')
prepopulated_fields = {'slug': ('name',)}
search_fields = ('name',)
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
list_display = ('title', 'slug', 'author', 'category', 'status', 'created')
list_filter = ('status', 'created', 'category', 'tags')
search_fields = ('title', 'body')
prepopulated_fields = {'slug': ('title',)}
raw_id_fields = ('author',)
date_hierarchy = 'created'
ordering = ('status', '-created')
filter_horizontal = ('tags',) # 多对多选择更友好
prepopulated_fields 使得在输入标题时自动生成 slug,filter_horizontal 给多对多字段一个美观的双栏选择器。
4.3 启动开发服务器
bash
python manage.py runserver
控制台输出:
bash
Watching for file changes with StatReloader
Performing system checks...
System check identified no issues (0 silenced).
May 16, 2026 - 10:15:20
Django version 4.2.5, using settings 'config.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
打开 http://127.0.0.1:8000/admin/,用超管账户登录。现在你可以随意创建分类、标签和文章了。
动手试一下:创建分类"Python"、标签"Django"和"Web",然后发布一篇测试文章,标题为"Hello Django"。这些数据将成为我们接下来开发视图和模板的素材。
5. 构建前端视图与 URL
我们希望博客具备以下页面:
-
文章列表(首页)
-
文章详情页
-
按分类筛选文章
-
按标签筛选文章
5.1 编写视图
打开 blog/views.py,从通用视图开始,最简洁高效:
bash
from django.views.generic import ListView, DetailView
from .models import Post, Category, Tag
class PostListView(ListView):
model = Post
context_object_name = 'posts'
template_name = 'blog/post_list.html'
paginate_by = 5
def get_queryset(self):
return Post.objects.filter(status='published').select_related('category', 'author').prefetch_related('tags')
class PostDetailView(DetailView):
model = Post
context_object_name = 'post'
template_name = 'blog/post_detail.html'
def get_queryset(self):
return Post.objects.filter(status='published').select_related('category', 'author').prefetch_related('tags')
def get_object(self, queryset=None):
# 使用 年/月/日/slug 作为查找字段
return Post.objects.filter(
status='published',
created__year=self.kwargs['year'],
created__month=self.kwargs['month'],
created__day=self.kwargs['day'],
slug=self.kwargs['slug']
).first()
class CategoryPostListView(ListView):
template_name = 'blog/post_list.html'
context_object_name = 'posts'
paginate_by = 5
def get_queryset(self):
self.category = Category.objects.get(slug=self.kwargs['category_slug'])
return Post.objects.filter(
status='published',
category=self.category
).select_related('category', 'author').prefetch_related('tags')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['category'] = self.category
return context
class TagPostListView(ListView):
template_name = 'blog/post_list.html'
context_object_name = 'posts'
paginate_by = 5
def get_queryset(self):
self.tag = Tag.objects.get(slug=self.kwargs['tag_slug'])
return Post.objects.filter(
status='published',
tags=self.tag
).select_related('category', 'author').prefetch_related('tags')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['tag'] = self.tag
return context
我们使用了 select_related 来优化外键查询,prefetch_related 来优化多对多查询,这是进阶中的常见性能优化。
5.2 配置 URL
在 blog 目录下新建 urls.py:
bash
from django.urls import path
from . import views
app_name = 'blog'
urlpatterns = [
path('', views.PostListView.as_view(), name='post_list'),
path('<int:year>/<int:month>/<int:day>/<slug:slug>/',
views.PostDetailView.as_view(), name='post_detail'),
path('category/<slug:category_slug>/',
views.CategoryPostListView.as_view(), name='category_detail'),
path('tag/<slug:tag_slug>/',
views.TagPostListView.as_view(), name='tag_detail'),
]
然后将博客的 URL 包含进项目主路由 config/urls.py:
bash
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('blog.urls')),
]
现在访问 http://127.0.0.1:8000/ 就会进入文章列表页(不过我们还没有模板)。
6. 模板设计:让博客"活"起来
6.1 基础模板
在 blog 应用下创建 templates/blog/ 目录(如果不存在)。首先创建 base.html:
bash
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}我的博客{% endblock %}</title>
<style>
body { font-family: sans-serif; max-width: 800px; margin: 0 auto; padding: 1em; }
nav a { margin-right: 1em; }
.post { border-bottom: 1px solid #eee; padding: 1em 0; }
.pagination { margin-top: 2em; }
.tags span { background: #eee; padding: 2px 6px; border-radius: 4px; margin-right: 4px; }
</style>
</head>
<body>
<nav>
<a href="{% url 'blog:post_list' %}">首页</a>
<a href="{% url 'admin:index' %}">管理</a>
</nav>
<hr>
{% block content %}
{% endblock %}
</body>
</html>
6.2 文章列表模板
创建 post_list.html:
bash
{% extends 'blog/base.html' %}
{% block title %}
{% if category %}{{ category.name }}{% elif tag %}{{ tag.name }}{% else %}文章列表{% endif %}
{% endblock %}
{% block content %}
<h1>
{% if category %}
分类:{{ category.name }}
{% elif tag %}
标签:{{ tag.name }}
{% else %}
最新文章
{% endif %}
</h1>
{% for post in posts %}
<div class="post">
<h2><a href="{{ post.get_absolute_url }}">{{ post.title }}</a></h2>
<p>
分类:<a href="{{ post.category.get_absolute_url }}">{{ post.category.name }}</a> |
作者:{{ post.author.username }} |
日期:{{ post.created|date:"Y-m-d" }}
</p>
<p>{{ post.body|truncatechars:150 }}</p>
<div class="tags">
{% for tag in post.tags.all %}
<span><a href="{{ tag.get_absolute_url }}">{{ tag.name }}</a></span>
{% endfor %}
</div>
</div>
{% empty %}
<p>还没有文章。</p>
{% endfor %}
{% if is_paginated %}
<div class="pagination">
<span>第 {{ page_obj.number }} 页,共 {{ paginator.num_pages }} 页</span>
{% if page_obj.has_previous %}
<a href="?page={{ page_obj.previous_page_number }}">上一页</a>
{% endif %}
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}">下一页</a>
{% endif %}
</div>
{% endif %}
{% endblock %}
6.3 文章详情模板
创建 post_detail.html:
bash
{% extends 'blog/base.html' %}
{% block title %}{{ post.title }}{% endblock %}
{% block content %}
<article>
<h1>{{ post.title }}</h1>
<p>
分类:<a href="{{ post.category.get_absolute_url }}">{{ post.category.name }}</a> |
作者:{{ post.author.username }} |
发布于:{{ post.created|date:"Y-m-d H:i" }}
{% if post.updated != post.created %}
| 更新于:{{ post.updated|date:"Y-m-d H:i" }}
{% endif %}
</p>
<div class="tags">
{% for tag in post.tags.all %}
<span><a href="{{ tag.get_absolute_url }}">{{ tag.name }}</a></span>
{% endfor %}
</div>
<hr>
<div>{{ post.body|linebreaks }}</div>
</article>
<a href="{% url 'blog:post_list' %}">← 返回列表</a>
{% endblock %}
现在重启服务器,刷新页面,你应该能看到之前从管理后台发布的文章。点击标题或"阅读更多"即可进入详情页。
7. 用 Shell 验证数据与查询
Django 的交互式 Shell 是调试和学习 ORM 的利器。运行:
你会看到:
bash
Python 3.10.12 (main, Nov 20 2023, 15:14:05) [GCC 11.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>>
7.1 创建示例数据
bash
from blog.models import Category, Tag, Post
from django.contrib.auth.models import User
user = User.objects.first() # 获取管理员用户
python_cat = Category.objects.create(name='Python', description='Python 相关文章')
django_cat = Category.objects.create(name='Django')
tag1 = Tag.objects.create(name='Django')
tag2 = Tag.objects.create(name='Web开发')
post = Post.objects.create(
title='深入 Django ORM',
body='Django 的 ORM 非常强大...',
author=user,
category=django_cat,
status='published'
)
post.tags.add(tag1, tag2)
再次执行创建会自动生成 slug,因为模型的 save 方法已处理。
7.2 查询与控制台输出
bash
# 获取所有已发布文章
published_posts = Post.objects.filter(status='published')
print(published_posts)
# 输出:<QuerySet [<Post: 深入 Django ORM>]>
# 带优化查询
posts = Post.objects.filter(status='published').select_related('category').prefetch_related('tags')
for p in posts:
print(f'{p.title} - 分类: {p.category.name} - 标签: {", ".join(t.name for t in p.tags.all())}')
# 输出:深入 Django ORM - 分类: Django - 标签: Django, Web开发
通过 Shell 你能直观感受 ORM 如何工作,这对理解视图层的查询逻辑非常有帮助。
8. 进阶优化与实用功能
8.1 Markdown 支持
博客文章用 Markdown 编写会更优雅。安装库:
在 Post 模型中添加一个方法,将 Markdown 转为 HTML:
bash
import markdown
class Post(models.Model):
# ... 字段省略 ...
def body_as_markdown(self):
return markdown.markdown(self.body, extensions=['extra', 'codehilite'])
在模板 post_detail.html 中,将 {{ post.body|linebreaks }} 替换为:
bash
{{ post.body_as_markdown|safe }}
注意 :markdown 库默认不处理代码高亮,可使用 Pygments 或简单引入 highlight.js 在模板中。
8.2 搜索功能
在 PostListView 中添加搜索支持。修改 get_queryset:
bash
class PostListView(ListView):
# ...
def get_queryset(self):
queryset = Post.objects.filter(status='published').select_related('category', 'author').prefetch_related('tags')
q = self.request.GET.get('q')
if q:
queryset = queryset.filter(title__icontains=q)
return queryset
并在 post_list.html 顶部添加搜索框:
bash
<form method="get" action="{% url 'blog:post_list' %}">
<input type="text" name="q" value="{{ request.GET.q }}" placeholder="搜索文章...">
<button type="submit">搜索</button>
</form>
8.3 显示所有分类与标签(侧边栏)
可以在列表和详情页的上下文中加入分类和标签,供全局导航使用。最简单的方法是在 base.html 中添加自定义模板标签。创建一个 templatetags 包:
bash
blog/
├── templatetags/
│ ├── __init__.py
│ └── blog_tags.py
blog_tags.py:
bash
from django import template
from ..models import Category, Tag
register = template.Library()
@register.inclusion_tag('blog/sidebar.html')
def show_sidebar():
categories = Category.objects.all()
tags = Tag.objects.all()
return {'categories': categories, 'tags': tags}
创建 templates/blog/sidebar.html:
bash
<h3>分类</h3>
<ul>
{% for cat in categories %}
<li><a href="{{ cat.get_absolute_url }}">{{ cat.name }}</a></li>
{% endfor %}
</ul>
<h3>标签</h3>
<div>
{% for tag in tags %}
<span style="background:#eee; padding:2px 6px; margin:2px;">
<a href="{{ tag.get_absolute_url }}">{{ tag.name }}</a>
</span>
{% endfor %}
</div>
在 base.html 中加载并使用:
bash
{% load blog_tags %}
...
<div style="float:right; width:200px;">
{% show_sidebar %}
</div>
<div style="margin-right:220px;">
{% block content %}{% endblock %}
</div>
这样所有页面都能显示分类和标签导航了。
9. 最终的控制台旅程
最后我们再通过控制台回顾一下项目的完整运行流程:
bash
# 1. 启动开发服务器
python manage.py runserver
# 输出:
# System check identified no issues (0 silenced).
# May 16, 2026 - 14:30:00
# Starting development server at http://127.0.0.1:8000/
# 2. 在另一个终端打开 Shell
python manage.py shell
bash
>>> from blog.models import Post
>>> Post.objects.filter(status='published').count()
5 # 假设我们已创建了5篇已发布文章
>>> p = Post.objects.first()
>>> p.title
'深入 Django ORM'
>>> p.body_as_markdown()
'<p>Django 的 ORM 非常强大...</p>'
当你访问 http://127.0.0.1:8000/,你会看到文章列表、分页、搜索框;点击分类或标签会精确过滤;管理后台 http://127.0.0.1:8000/admin/ 让内容管理直观高效。这一切都在不到 300 行核心代码内完成。
10. 总结与下一步
至此,你已经从零开始,利用 Django 构建了一个功能完备的博客系统:
-
模型设计:Category、Tag、Post,理解一对多与多对多
-
管理后台:定制化显示、筛选、搜索
-
视图与模板:基于类的通用视图、模板继承、分页
-
URL 设计:友好的年/月/日/slug 结构
-
性能优化:select_related 和 prefetch_related
-
进阶功能:Markdown 渲染、搜索、侧边栏自定义标签
接下来你可以继续扩展:
-
评论系统(django-comments 或自建模型)
-
RSS 订阅功能(Django 内置 syndication 框架)
-
站点地图(sitemap)
-
使用 PostgreSQL 并部署至生产环境(如 Railway、Heroku 或 VPS)
如果觉得不错,还可以去公众号、今日头条搜索「IT策士」,一起升级 IT 思维 !