简介:直接下载就能跑的Django博客项目,用SQLite做本地数据库,不用额外装MySQL或PostgreSQL。代码里已经写好文章、分类、标签的模型结构,配好了迁移文件,执行python manage.py migrate自动建表,再用python manage.py runserver就能打开首页和后台。前台有文章列表页、详情页、按分类筛选功能;后台支持登录后发布、编辑、删除文章,管理分类和标签,所有操作都在Django admin里完成。项目包含完整的templates模板(HTML结构清晰)、static静态资源(CSS和基础JS)、urls路由配置、views视图逻辑、models数据模型和admin后台注册代码。附带README.md,一步步说明怎么安装依赖(pip install -r requirements.txt)、初始化数据库、启动服务。.idea是PyCharm配置,不影响运行,可删。适合刚学完Python想动手搭Web应用的人,边跑边理解Django的MTV流程:请求怎么从URL进来到view处理,怎么查models数据,再渲染到template展示。
1. 项目概述:为什么这个Django博客是新手真正能“摸得着”的起点
你是不是也经历过这样的时刻:翻完三本Django教程,把models.py、views.py、urls.py这些名词背得滚瓜烂熟,可一打开编辑器,光是新建一个app就卡在python manage.py startapp blog之后——接下来该写什么?models.py里字段怎么定义才算合理?admin.py注册了模型却看不到后台入口?urls.py嵌套两层就绕晕了?更别说模板里{% for post in posts %}到底从哪来的posts变量……理论和实操之间,隔着一道看不见的墙。
这个项目就是专门来拆掉这堵墙的。它不是教学视频里的“演示代码”,也不是文档里零散的代码片段,而是一个完整、自洽、开箱即用的最小可行产品(MVP)。它用SQLite作为数据库,意味着你不需要安装、配置、维护任何外部服务——没有MySQL的root密码问题,没有PostgreSQL的端口冲突,没有环境变量的反复调试。db.sqlite3文件就在项目根目录下,双击就能用DB Browser打开看数据;migrations/目录里预置了0001_initial.py,执行python manage.py migrate后,五张表(文章、分类、标签、用户、评论)瞬间生成;python manage.py runserver回车,浏览器输入http://127.0.0.1:8000,首页立刻加载出三篇示例文章;再访问http://127.0.0.1:8000/admin,用python manage.py createsuperuser创建的账号登录,后台管理界面干净利落,所有字段所见即所得。
关键词里说的“django博客”、“sqlite本地开发”、“python web项目”、“后台内容管理”,在这里不是抽象概念,而是你能亲手点击、拖拽、编辑、删除的具体对象。比如,你在后台新建一个“Python技巧”分类,前台文章列表页的侧边栏分类导航就自动多出一项;你给某篇文章打上“Django”和“Web开发”两个标签,点进标签页就能看到所有带这两个标签的文章聚合;你修改一篇文章的标题或正文,刷新前台详情页,变化秒级生效。这种“所见即所得”的反馈闭环,对建立学习信心至关重要——它让你确信:Django不是魔法,而是由清晰模块组成的精密齿轮组,每个齿轮咬合的位置、转动的方向、传递的力量,都是可观察、可干预、可复现的。
更重要的是,它的结构设计直指Django核心范式MTV(Model-Template-View),而非过时的MVC。models.py定义数据是什么(What),views.py决定数据怎么查、怎么处理(How),templates/下的HTML文件专注数据怎么呈现(How it looks)。你看blog/views.py里post_list函数,它不负责渲染HTML,只做一件事:从models.Post里filter(published_date__isnull=False).order_by('-published_date')取出已发布的文章,然后把结果打包成字典{'posts': posts},交给render()函数去匹配blog/templates/blog/post_list.html。这个分离逻辑,正是Django工程化协作的基础。当你未来要接入Redis缓存热门文章列表,只需在views.py里加几行cache.get()和cache.set(),模板和模型完全不用动;当你想换一套前端框架,比如用Vue重写前台,views.py只需把render()换成JsonResponse()返回JSON数据,后端逻辑依然健壮如初。这个项目,就是你理解这种分层思想最扎实的脚手架。
2. 整体架构与设计思路:为什么选择SQLite+Django Admin+基础模板的组合
2.1 数据库选型:SQLite不是妥协,而是精准匹配学习场景的利器
很多教程一上来就教MySQL或PostgreSQL,理由很充分:生产环境都用它们。但对新手而言,这恰恰是最大的认知负担。安装MySQL需要下载安装包、配置环境变量、初始化数据目录、设置root密码、创建新用户、授权数据库权限……每一步都可能因系统差异(Windows/macOS/Linux)、版本冲突(MySQL 5.7 vs 8.0)、防火墙拦截而失败。一个OperationalError: (2003, "Can't connect to MySQL server on 'localhost'")报错,足以让初学者怀疑人生。
SQLite完美规避了所有这些问题。它不是一个需要独立运行的服务进程,而是一个嵌入式数据库引擎,以单个.sqlite3文件形式存在。Django对它的支持是原生且无缝的。你只需要在settings.py里确认这一段配置:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
BASE_DIR / 'db.sqlite3'指向的就是项目根目录下的db.sqlite3文件。第一次执行migrate时,Django会自动创建这个文件;后续所有makemigrations和migrate操作,都只是读写这个文件的内容。你可以把它复制到U盘,拿到另一台电脑上,只要装好Python和Django,runserver就能立刻跑起来。这种“零配置、零依赖、零运维”的特性,让学习者能100%聚焦在Django本身——模型字段类型怎么选(CharField vs TextField)、外键关系如何建立(ForeignKey的on_delete参数为何必须指定)、查询集(QuerySet)的惰性执行机制如何工作。我试过让完全没接触过数据库的同学,在两小时内完成从安装Python到在后台发布第一篇文章的全过程,关键就在于SQLite抹平了所有环境障碍。
当然,SQLite有明确的适用边界:它不支持高并发写入(不适合日活百万的新闻站),不提供用户权限管理(所有连接拥有同等权限),也不具备复杂的存储过程功能。但这些恰恰是学习阶段最不需要的。当你需要处理高并发时,Django的数据库抽象层(ORM)让你只需修改settings.py里的ENGINE和NAME,其他所有models.py、views.py代码几乎无需改动——这才是Django“一次编写,多数据库适配”的真正价值。这个项目用SQLite,不是教你“只能用SQLite”,而是教你“先用最简单的工具,把核心逻辑跑通”。
2.2 后台管理:Django Admin不是玩具,而是被严重低估的生产力核弹
新手常误以为Django Admin只是一个“演示用的后台”,觉得真正的网站必须自己手写CRUD页面。这是巨大的误解。Django Admin是Django框架最成熟、最稳定、最安全的模块之一,其背后是经过十余年生产环境锤炼的权限控制、数据验证、日志审计和UI组件库。在这个项目里,blog/admin.py的几行代码,就构建了一个功能完备的内容管理系统:
from django.contrib import admin
from .models import Post, Category, Tag
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
list_display = ('title', 'author', 'category', 'published_date', 'status')
list_filter = ('status', 'created_date', 'published_date', 'category', 'tags')
search_fields = ('title', 'content')
prepopulated_fields = {'slug': ('title',)}
date_hierarchy = 'published_date'
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
prepopulated_fields = {'slug': ('name',)}
@admin.register(Tag)
class TagAdmin(admin.ModelAdmin):
prepopulated_fields = {'slug': ('name',)}
这段代码带来的能力远超表面所见:
- list_display定义了后台文章列表页显示哪些字段,author和category会自动变成可点击的链接,点进去就能直接跳转到对应作者或分类的编辑页;
- list_filter在右侧生成筛选面板,你可以一键筛选“草稿”状态的文章,或按“2024年6月”发布的文章进行归档;
- search_fields启用全文搜索,输入“Django”就能找到所有标题或内容包含该词的文章;
- prepopulated_fields是神来之笔:当你输入文章标题“Django MTV架构详解”,slug字段会自动填充为django-mtv-jiegou-xiangjie,这直接解决了SEO友好的URL生成问题,避免了手动输入易出错的连字符;
- date_hierarchy在顶部添加了按年/月/日逐级下钻的时间导航条,管理大量历史文章时效率倍增。
这些功能,如果全部手写,至少需要数百行HTML/CSS/JS代码,还要处理表单验证、CSRF防护、权限校验等安全细节。而Django Admin用声明式语法(declarative syntax)几行搞定。它强制你思考数据的业务含义(哪些字段该展示?哪些该筛选?哪些该搜索?),而不是陷入技术实现的泥潭。我带过的学员中,有位做企业内训的讲师,用这个项目模板三天内就搭出了公司内部的技术文档库,所有部门经理都能通过Admin后台自助发布、更新文档,完全不需要前端开发介入。这就是Django Admin的真实威力:它把内容管理的复杂度,从“写代码”降维到“配配置”。
2.3 模板与静态资源:基础不等于简陋,结构清晰才是可扩展的前提
项目里的templates/目录,乍看只有几个HTML文件:base.html、post_list.html、post_detail.html、category_list.html、tag_list.html。没有炫酷的动画,没有响应式框架,甚至CSS都写在static/css/style.css里,没有用Webpack打包。但这恰恰是深思熟虑的设计。
base.html是整个站点的骨架,定义了所有页面共用的<head>元信息、导航栏、页脚和内容占位符:
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}我的博客{% endblock %}</title>
<link rel="stylesheet" href="{% static 'css/style.css' %}">
</head>
<body>
<header>
<nav>
<a href="{% url 'post_list' %}">首页</a>
<a href="{% url 'post_list' %}?category=python">Python</a>
<!-- 更多分类链接 -->
</nav>
</header>
<main>
{% block content %}{% endblock %}
</main>
<footer>© 2024 我的博客</footer>
</body>
</html>
post_list.html则继承它,并填充具体内容:
{% extends 'base.html' %}
{% block title %}文章列表 - {{ block.super }}{% endblock %}
{% block content %}
<h1>最新文章</h1>
{% for post in posts %}
<article>
<h2><a href="{{ post.get_absolute_url }}">{{ post.title }}</a></h2>
<p class="meta">{{ post.published_date|date:"Y年m月d日" }} | 分类:{{ post.category.name }}</p>
<p>{{ post.content|truncatewords:50 }}</p>
<a href="{{ post.get_absolute_url }}">阅读全文 →</a>
</article>
{% endfor %}
{% endblock %}
这种extends/block机制,是Django模板语言的核心优势。它确保了视觉一致性(所有页面共享同一套CSS和布局),又保证了灵活性(每个子页面只关注自己的业务逻辑)。当你未来想给博客增加夜间模式,只需在base.html的<head>里加一段JavaScript切换CSS类,所有页面立即生效;想在页脚增加社交媒体图标,改一处base.html,全站同步更新。这种“一处修改,全局生效”的能力,是任何拼凑式HTML都无法比拟的。
静态资源(CSS/JS/图片)放在static/目录下,通过{% static 'css/style.css' %}模板标签引用,Django在开发模式下会自动处理静态文件的URL路径。虽然项目没用Bootstrap或Tailwind CSS,但style.css里的基础样式(如.meta类定义字体大小和颜色、article的间距)已经足够清晰传达信息层级。这为后续学习前端框架提供了绝佳的“空白画布”——你可以把style.css整个替换成Bootstrap的CDN链接,或者用npm init引入Vue,而Django后端逻辑纹丝不动。基础模板的价值,不在于它有多美,而在于它有多“干净”、多“标准”,让你能毫无阻碍地向上叠加新技术。
3. 核心模块深度解析:从models到views,每一行代码都在讲一个故事
3.1 数据模型(models.py):用Python类定义现实世界的规则
blog/models.py是整个项目的基石,它用Python类描述了博客世界里的实体及其关系。我们逐行拆解,看它如何将“文章”、“分类”、“标签”这些抽象概念,转化为可存储、可查询、可关联的数据结构。
from django.db import models
from django.contrib.auth.models import User
from django.urls import reverse
from django.utils import timezone
class Category(models.Model):
name = models.CharField('分类名', max_length=100)
slug = models.SlugField('URL别名', max_length=100, unique=True)
created_date = models.DateTimeField('创建时间', auto_now_add=True)
class Meta:
verbose_name = '分类'
verbose_name_plural = '分类'
ordering = ['name']
def __str__(self):
return self.name
class Tag(models.Model):
name = models.CharField('标签名', max_length=100)
slug = models.SlugField('URL别名', max_length=100, unique=True)
created_date = models.DateTimeField('创建时间', auto_now_add=True)
class Meta:
verbose_name = '标签'
verbose_name_plural = '标签'
ordering = ['name']
def __str__(self):
return self.name
class Post(models.Model):
STATUS_CHOICES = (
('draft', '草稿'),
('published', '已发布'),
)
title = models.CharField('标题', max_length=200)
slug = models.SlugField('URL别名', max_length=200, unique=True)
author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='作者')
category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, blank=True, verbose_name='分类')
tags = models.ManyToManyField(Tag, blank=True, verbose_name='标签')
content = models.TextField('正文')
excerpt = models.CharField('摘要', max_length=200, blank=True)
published_date = models.DateTimeField('发布时间', null=True, blank=True)
created_date = models.DateTimeField('创建时间', auto_now_add=True)
updated_date = models.DateTimeField('更新时间', auto_now=True)
class Meta:
verbose_name = '文章'
verbose_name_plural = '文章'
ordering = ['-published_date', '-created_date']
def __str__(self):
return self.title
def get_absolute_url(self):
return reverse('post_detail', kwargs={'slug': self.slug})
首先看Category和Tag模型。它们结构高度相似,体现了Django的DRY(Don’t Repeat Yourself)原则。name字段用CharField存储名称,max_length=100是经验性约束——没人会给分类起超过100个字符的名字;slug字段用SlugField,它本质是CharField的子类,但内置了URL安全验证(只允许字母、数字、连字符和下划线),unique=True确保每个分类的URL别名全球唯一,避免/category/python/和/category/Python/产生歧义;created_date用auto_now_add=True,意味着该字段在对象首次保存到数据库时,自动填入当前服务器时间,无需手动赋值。
Post模型是核心,它串联起了所有实体。STATUS_CHOICES是一个元组构成的选项列表,定义了文章的两种状态:“草稿”和“已发布”。这个设计非常关键:它让数据库层面就约束了数据的有效性,避免出现status='pending'这种非法值。author字段是ForeignKey,建立了文章与Django内置User模型的一对多关系。“一对多”意味着一个用户可以写多篇文章,但一篇文章只能有一个作者。on_delete=models.CASCADE表示:如果某个用户被删除,他写的所有文章也会被级联删除(这是内容型网站的常见策略,避免孤儿文章)。而category字段的on_delete=models.SET_NULL则不同:如果某个分类被删除,该分类下的所有文章category字段会被设为NULL(空值),而不是被删掉,这样文章内容得以保留,只是失去了分类归属。
最精妙的是tags字段,它使用ManyToManyField。这表示文章和标签是多对多关系:一篇文章可以有多个标签(如“Django”和“Web开发”),一个标签也可以被多篇文章使用(所有讲Django的文章都打上“Django”标签)。Django ORM会自动为你创建一张中间表(blog_post_tags),里面只有两列:post_id和tag_id,完美解决关系映射。blank=True允许文章不打任何标签,这对内容管理极其友好。
get_absolute_url()方法是Django的约定俗成。它返回一个字符串,代表该文章实例的唯一URL。reverse('post_detail', kwargs={'slug': self.slug})中的'post_detail'是urls.py里给详情页路由起的名字,kwargs传入动态参数。这样,在模板里调用post.get_absolute_url,就能得到类似/post/django-mtv-jiegou-xiangjie/的链接。这个方法的好处是:如果未来你把详情页URL从/post/<slug>/改成/article/<slug>/,只需修改urls.py里的path定义,所有模板里的链接自动更新,无需逐个查找替换。
3.2 视图逻辑(views.py):请求-响应循环的中枢神经
blog/views.py是Django MTV架构中承上启下的关键环节。它接收来自urls.py的HTTP请求,调用models.py获取数据,再将数据传递给templates/进行渲染,最终生成HTTP响应。这里的代码,每一行都在回答一个问题:“当用户访问这个URL时,我该做什么?”
from django.shortcuts import render, get_object_or_404
from django.core.paginator import Paginator
from .models import Post, Category, Tag
def post_list(request):
"""文章列表页视图"""
# 获取所有已发布的文章,按发布时间倒序排列
posts = Post.objects.filter(published_date__isnull=False).order_by('-published_date')
# 处理分类筛选
category_slug = request.GET.get('category')
if category_slug:
category = get_object_or_404(Category, slug=category_slug)
posts = posts.filter(category=category)
# 处理标签筛选
tag_slug = request.GET.get('tag')
if tag_slug:
tag = get_object_or_404(Tag, slug=tag_slug)
posts = posts.filter(tags=tag)
# 分页处理,每页显示5篇文章
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,
'categories': Category.objects.all(),
'tags': Tag.objects.all(),
})
def post_detail(request, slug):
"""文章详情页视图"""
post = get_object_or_404(Post, slug=slug, published_date__isnull=False)
return render(request, 'blog/post_detail.html', {'post': post})
def category_list(request, slug):
"""分类列表页视图"""
category = get_object_or_404(Category, slug=slug)
posts = Post.objects.filter(category=category, published_date__isnull=False).order_by('-published_date')
return render(request, 'blog/category_list.html', {
'category': category,
'posts': posts,
'categories': Category.objects.all(),
'tags': Tag.objects.all(),
})
def tag_list(request, slug):
"""标签列表页视图"""
tag = get_object_or_404(Tag, slug=slug)
posts = Post.objects.filter(tags=tag, published_date__isnull=False).order_by('-published_date')
return render(request, 'blog/tag_list.html', {
'tag': tag,
'posts': posts,
'categories': Category.objects.all(),
'tags': Tag.objects.all(),
})
post_list函数是流量入口,它处理了三种核心场景:首页展示、分类筛选、标签筛选。Post.objects.filter(published_date__isnull=False)是Django ORM的查询语法,__isnull=False是字段查找(lookup)的一种,意思是“published_date字段不为空”,这巧妙地过滤掉了所有草稿文章,实现了内容发布的状态控制。request.GET.get('category')从URL查询参数中提取category的值,比如访问/blog/?category=python,这里就得到'python'。get_object_or_404是一个便捷函数,它尝试用slug='python'查找Category对象,如果找不到,就自动返回一个HTTP 404错误页面,省去了手动写try...except的麻烦。
分页逻辑是另一个亮点。Paginator(posts, 5)创建一个分页器,告诉Django“把posts这个查询集,每页显示5条”。request.GET.get('page')获取URL中的page参数(如/blog/?page=2),paginator.get_page(page_number)则返回第2页的数据。最终传递给模板的page_obj,是一个包含了当前页数据、总页数、是否有上一页/下一页等丰富属性的对象。在post_list.html模板里,你可以用{% for post in page_obj %}遍历当前页文章,用{{ page_obj.has_previous }}判断是否有上一页,用{{ page_obj.previous_page_number }}获取上一页的页码。这种将复杂逻辑封装在Python对象中,再通过简单模板语法暴露给前端的设计,极大降低了模板的复杂度。
post_detail函数则展示了Django的“约定优于配置”哲学。get_object_or_404(Post, slug=slug, published_date__isnull=False)一行代码,同时完成了三个任务:根据URL中的slug参数查找文章、确保该文章已发布(published_date不为空)、如果查找失败则返回404。这比手写SQL查询再判断结果是否存在,简洁高效了十倍。slug参数来自urls.py的path('post/<slug:slug>/', views.post_detail, name='post_detail'),Django自动将其提取并传入视图函数,开发者只需专注业务逻辑。
3.3 URL路由(urls.py):定义应用的“交通地图”
blog/urls.py和项目根目录的urls.py共同构成了Django应用的URL分发系统。它像一张交通地图,告诉Django:“当用户访问/post/django-mtv-jiegou-xiangjie/时,请把请求交给blog.views.post_detail函数处理。”
# blog/urls.py
from django.urls import path
from . import views
app_name = 'blog' # 命名空间,防止URL名称冲突
urlpatterns = [
path('', views.post_list, name='post_list'),
path('post/<slug:slug>/', views.post_detail, name='post_detail'),
path('category/<slug:slug>/', views.category_list, name='category_list'),
path('tag/<slug:slug>/', views.tag_list, name='tag_list'),
]
app_name = 'blog'是关键。它为这个应用的所有URL定义了一个命名空间。这意味着,在模板里引用URL时,必须写成{% url 'blog:post_list' %},而不是简单的{% url 'post_list' %}。这样做的好处是,如果你未来在同一个项目里再添加一个news应用,它也有一个post_list视图,命名空间能确保两者互不干扰。path函数的参数中,<slug:slug>是一个路径转换器(path converter),它告诉Django:“这个位置的URL片段,应该被当作一个slug类型的字符串,并赋值给视图函数的slug参数。”slug转换器内置了正则表达式验证,只接受URL安全的字符,有效防止了恶意输入。
根目录的urls.py则负责“总调度”:
# mysite/urls.py (假设项目名为mysite)
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('blog.urls')), # 将所有以空字符串开头的URL,转发给blog应用处理
]
# 开发模式下,让Django能直接提供静态文件(生产环境应由Nginx处理)
if settings.DEBUG:
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
include('blog.urls')是Django模块化的核心。它把blog/urls.py里定义的所有路径,都挂载到根URL下。所以blog/urls.py里的path('', ...),最终对应的是/(首页),而path('post/<slug:slug>/', ...)则对应/post/<slug>/。这种分层设计,让大型项目可以轻松拆分成多个独立应用,每个应用只关心自己的URL,互不耦合。
3.4 后台管理(admin.py):用配置代替编码的终极实践
blog/admin.py再次印证了Django“配置驱动”的强大。它没有一行业务逻辑代码,全是声明式的配置,却构建了一个功能远超手写后台的管理界面。
from django.contrib import admin
from .models import Post, Category, Tag
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
list_display = ('title', 'author', 'category', 'published_date', 'status')
list_filter = ('status', 'created_date', 'published_date', 'category', 'tags')
search_fields = ('title', 'content')
prepopulated_fields = {'slug': ('title',)}
date_hierarchy = 'published_date'
# 高级配置:自定义表单字段顺序和分组
fieldsets = (
('基本信息', {'fields': ('title', 'slug', 'author', 'category', 'tags')}),
('内容', {'fields': ('content', 'excerpt')}),
('状态与时间', {'fields': ('status', 'published_date')}),
)
# 只读字段:创建后不能修改
readonly_fields = ('created_date', 'updated_date')
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
prepopulated_fields = {'slug': ('name',)}
list_display = ('name', 'slug', 'created_date')
@admin.register(Tag)
class TagAdmin(admin.ModelAdmin):
prepopulated_fields = {'slug': ('name',)}
list_display = ('name', 'slug', 'created_date')
fieldsets配置将后台编辑表单的字段进行了逻辑分组。“基本信息”、“内容”、“状态与时间”三大区块,让编辑者一眼看清数据结构,避免在几十个字段中迷失。readonly_fields则定义了created_date和updated_date为只读,因为它们由auto_now_add和auto_now自动管理,人工修改既无意义又易出错。这种细粒度的控制,是手写后台难以企及的。
list_display不仅决定显示什么,还决定了交互方式。author和category字段名前缀'author__username'或'category__name',可以显示关联对象的特定字段(如作者用户名而非User object (1)),但项目里用了默认的__str__方法,所以显示的是author.__str__()返回的字符串(通常是用户名)。list_filter的'tags'项,会自动生成一个多选筛选器,让你能勾选“Django”和“Web开发”两个标签,一次性筛选出同时拥有这两个标签的文章,这是多对多关系的天然优势。
4. 实操全流程:从零开始,十分钟跑通你的第一个Django博客
4.1 环境准备与依赖安装:三步走,告别环境地狱
第一步,确认Python版本。这个项目基于Django 4.x,要求Python 3.8或更高版本。在终端(macOS/Linux)或命令提示符(Windows)中输入:
python --version
# 如果输出类似 Python 3.9.7,则符合要求
# 如果提示 'python' 不是内部或外部命令,请用 python3 --version
第二步,创建并激活虚拟环境。这是Python项目的黄金法则,它能隔离项目依赖,避免不同项目间的包版本冲突。在项目根目录(即包含manage.py的目录)下执行:
# macOS/Linux
python3 -m venv venv
source venv/bin/activate
# Windows
python -m venv venv
venv\Scripts\activate.bat
激活后,命令行提示符前会多出(venv)字样,表示你已进入虚拟环境。
第三步,安装依赖。项目根目录下的requirements.txt文件,精确记录了所有必需的Python包及其版本:
pip install -r requirements.txt
requirements.txt内容通常如下:
Django==4.2.7
Pillow==10.0.1
# 其他可能的包...
pip install -r会严格按照文件里的版本号安装,确保你的环境与项目作者完全一致。我曾遇到一个学员,因为本地Django版本是5.0,而项目代码是为4.2编写的,导致admin.py里的某些API调用报错。requirements.txt就是解决这类问题的终极保险。
提示:如果
pip install过程中遇到网络问题,可以临时更换国内镜像源,例如清华源:pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple/
4.2 数据库初始化与超级用户创建:让数据活起来
项目已经预置了迁移文件(blog/migrations/0001_initial.py),你无需自己运行makemigrations。直接执行:
python manage.py migrate
这条命令会读取migrations/目录下的所有迁移文件,对比当前数据库状态,生成并执行必要的SQL语句。对于全新的db.sqlite3文件,它会创建五张表:auth_user(用户表)、blog_category、blog_tag、blog_post、blog_post_tags(多对多中间表)。执行成功后,你会看到类似Applying blog.0001_initial... OK的输出。
接下来,创建一个超级用户(superuser),这是登录Django Admin后台的唯一凭证:
python manage.py createsuperuser
系统会依次提示你输入:
- Username: (建议输入admin,简单好记)
- Email address: (可选,直接回车跳过)
- Password: (输入密码,注意终端不会显示字符)
- Password (again): (再次输入密码确认)
密码输入时屏幕是黑的,这是正常的安全设计,放心输入即可。创建成功后,你就可以用这个账号登录后台了。
注意:
createsuperuser命令只创建用户,不赋予任何额外权限。Django超级用户默认拥有所有模型的全部权限(add/change/delete/view),这是它被称为“超级”的原因。普通用户需要在Admin后台的“Groups”或“User permissions”里手动分配权限。
4.3 启动开发服务器与首次访问:见证奇迹的时刻
一切就绪,启动Django内置的开发服务器:
python manage.py runserver
你会看到类似输出:
Watching for file changes with StatReloader
Performing system checks...
System check identified no issues (0 silenced).
June 15, 2024 - 10:23:45
Django version 4.2.7, using settings 'mysite.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
现在,打开你的浏览器,访问 http://127.0.0.1:8000。你应该能看到一个简洁的博客首页,上面显示着几篇示例文章。点击任意文章标题,进入详情页,查看完整的Markdown格式内容(项目里content字段是纯文本,如需支持Markdown渲染,可后续集成django-markdownx等包)。
接着,访问后台地址 http://127.0.0.1:8000/admin。输入你刚才创建的用户名和密码,登录后,你会看到一个干净的管理界面,左侧菜单栏清晰列出了“文章”、“分类”、“标签”、“用户”等模型。点击“文章”,就能看到所有已发布的文章列表,右侧的“ADD POST”按钮,让你可以立刻开始撰写自己的第一篇博客。
4.4 模板与静态资源的本地化改造:让你的博客真正属于你
项目提供的模板是通用的,但你的博客需要个性。修改templates/blog/base.html是最直接的方式。找到<title>标签:
<title>{% block title %}我的博客{% endblock %}</title>
把我的博客改成你想要的名字,比如张三的技术笔记。再找到页脚:
<footer>© 2024 我的博客</footer>
改成© 2024 张三的技术笔记。保存文件,刷新浏览器,变化立刻生效。
静态资源的修改同样简单。static/css/style.css是唯一的CSS文件。如果你想把主题色从默认的蓝色改成绿色,找到类似color: #007bff;的声明(这是Bootstrap的默认蓝色),把它改成color: #28a745;(Bootstrap的绿色)。保存后,所有文字颜色都会变绿。
实操心得:Django开发模式下,静态文件的修改是实时生效的,无需重启服务器。但如果你修改了Python代码(如
views.py或models.py),runserver会自动检测到文件变更并热重载(hot reload),你只需刷新浏览器即可看到效果。这是Django开发体验如此流畅的关键。
5. 常见问题与排查技巧实录:那些踩过的坑,我都帮你趟平了
5.1 “ModuleNotFoundError: No module named ‘blog’” —— 应用未注册的静默陷阱
现象:执行python manage.py migrate或runserver时,报错ModuleNotFoundError: No module named 'blog'。
原因:Django不知道blog是一个应用。这通常是因为在项目根目录的settings.py文件中,INSTALLED_APPS列表里漏掉了'blog'。
排查步骤:
1. 打开mysite/settings.py(mysite是你的项目名)。
2. 找到INSTALLED_APPS变量,它是一个元组或列表。
3. 确认其中是否包含'blog'。正确的写法是:
python INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', # ... 其他内置应用 'blog', # 这一行必须存在! ]
4. 如果没有,添加上去,保存文件。
为什么容易忽略:很多教程会说“记得把应用加到INSTALLED_APPS”,但新手往往以为这只是为了后台显示,不知道它其实是Django发现模型、视图、模板的“注册中心”。没有它,Django根本不会扫描blog/models.py,自然找不到blog模块。
5.2 “NoReverseMatch at /” —— URL名称不匹配的迷雾
现象:访问首页时,浏览器显示Reverse for 'post_list' not found.错误。
原因:模板里写了{% url 'post_list' %},但Django在urls.py里找不到名为'post_list'的URL模式。
排查步骤:
1. 检查blog/urls.py,确认path('', views.post_list, name='post_list')这一行存在,且name='post_list'拼写正确(注意是单引号,且没有多余空格)。
2. 检查mysite/urls.py,确认include('blog.urls')已正确导入,并且path('', include('blog.urls'))的路径是空字符串'',这样才能让blog/urls.py里的path('')匹配到根URL /。
3. 如果你修改过app_name,比如设成了app_name = 'myblog',那么模板里必须写成{% url 'myblog:post_list' %},否则Django无法定位。
避坑技巧:在blog/urls.py顶部加上print("Blog URLs loaded"),然后启动服务器。如果控制台没打印这句话,说明include()没生效,问题一定出在mysite/urls.py的导入路径上。
5.3 “The ‘slug’ field cannot be null” —— 表单提交失败的无声崩溃
现象:在后台新建文章时,点击“SAVE”后页面没有任何反应,或者跳转回编辑页,但文章并未保存,且slug字段下方出现红色错误提示:“此字段不能为空”。
原因:Post模型的slug字段定义为null=False(默认行为),但后台表单没有为它提供默认值。prepopulated_fields只在JavaScript层面生效,如果用户禁用了JS,或者手动清空了slug字段,提交就会失败。
解决方案:
1. 最佳实践:在models.py中,为slug字段添加blank=True,并修改save()方法,确保在保存前自动生成:
```python
from django.utils.text import slugify
class Post(models.Model):
# … 其他字段 …
slug = models.SlugField(‘URL别名’, max_length=200, unique=True, blank=True)
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.title)
super().save(*args, **kwargs)
`` 2. **快速修复**:在admin.py中,将slug字段加入readonly_fields,让它在后台不可编辑,完全依赖prepopulated_fields`的JS生成。
为什么重要:这个错误揭示了Django的一个核心理念——模型层的约束永远高于表单层。即使你在Admin后台做了JS自动填充,模型层的null=False依然是最后一道防线。学会在模型层加固数据完整性,是写出健壮Web应用的第一课。
5.4 “Page not found (404)” —— URL路径与视图函数的错位
现象:访问/post/my-first-post/时,返回404页面,但后台里这篇文章明明存在,且slug是my-first-post。
排查清单:
- ✅ 确认文章的published_date字段不为空。post_detail视图里有published_date__isnull=False的过滤条件,草稿文章无法通过此URL访问。
- ✅ 确认URL中的slug与数据库里post.slug的值完全一致(区分大小写、连字符)。在SQLite中,'My-First-Post'和'my-first-post'是不同的值。
- ✅ 在views.py的post_detail函数开头,临时添加一行日志:
python print(f"Looking for slug: {slug}")
启动服务器,访问URL,查看终端输出的slug值,再用DB Browser打开db.sqlite3,直接查询SELECT * FROM blog_post WHERE slug = 'my-first-post';,确认数据是否存在。
终极武器:在mysite/urls.py的urlpatterns末尾,添加一个“兜底”路由,用于调试:
from django.http import HttpResponse
urlpatterns += [
path('debug/<path:rest>/', lambda request, rest: HttpResponse(f"Debug: {rest}")),
]
访问/debug/post/my-first-post/,如果能看到输出,说明URL路由本身没问题,问题一定出在视图逻辑里。
5.5 “Invalid HTTP_HOST header” —— 生产部署前的预警信号
现象:在本地开发时一切正常,但当你尝试用python manage.py runserver 0.0.0.0:8000让局域网内其他设备访问时,浏览器显示DisallowedHost at /错误。
原因:Django的安全机制,默认只允许ALLOWED_HOSTS列表中的域名或IP访问。0.0.0.0:8000是一个通配地址,Django无法确定哪个具体的主机名是可信的。
安全修复:
1. 打开mysite/settings.py。
2. 找到ALLOWED_HOSTS变量。
3. 将其修改为:
python ALLOWED_HOSTS = ['127.0.0.1', 'localhost', '192.168.1.100'] # 替换为你的本机局域网IP
或者,仅在开发时,用通配符(不推荐用于生产环境):
python ALLOWED_HOSTS = ['*']
为什么必须重视:这个错误是Django防止“HTTP Host头攻击”的第一道屏障。攻击者可以伪造Host头,诱导你的应用生成错误的绝对URL(如https://evil.com/static/js/bad.js),从而窃取用户Cookie。ALLOWED_HOSTS是生产环境部署时的必填项,提前了解它,能让你在未来上线时少走弯路。
6. 进阶拓展与个人定制:从“能跑”到“好用”的跃迁路径
6.1 内容增强:为文章添加Markdown支持与代码高亮
纯文本内容在技术博客中显得苍白。集成Markdown能让文章排版更专业,代码块自动高亮。推荐使用django-markdownx:
pip install django-markdownx
在settings.py中添加:
INSTALLED_APPS += ['markdownx',]
修改blog/models.py的Post.content字段:
from markdownx.models import MarkdownxField
class Post(models.Model):
# ... 其他字段 ...
content = MarkdownxField('正文') # 替换原来的 TextField
在blog/admin.py中,为content字段指定一个富文本编辑器:
from markdownx.admin import MarkdownxModelAdmin
@admin.register(Post)
class PostAdmin(MarkdownxModelAdmin): # 继承自 MarkdownxModelAdmin
# ... 其他配置保持不变 ...
最后,在blog/templates/blog/post_detail.html中,将{{ post.content }}改为{{ post.content|safe }},让Markdown渲染后的HTML能被正确解析。|safe过滤器告诉Django:“这段内容是可信的,不要转义HTML标签。”
6.2 性能优化:为首页添加缓存,让访问快如闪电
当文章数量增长到上百篇,每次访问首页都要执行一次数据库查询,会成为性能瓶颈。Django内置的缓存框架可以轻松解决:
在settings.py中配置缓存后端(开发模式用内存缓存):
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'LOCATION': 'unique-snowflake',
}
}
在blog/views.py的post_list函数中,包裹数据库查询:
from django.core.cache import cache
def post_list(request):
# 生成一个唯一的缓存键
cache_key = f'post_list_{request.GET.urlencode()}'
posts = cache.get(cache_key)
if posts is None:
# 缓存未命中,执行数据库查询
posts = Post.objects.filter(published_date__isnull=False).order_by('-published_date')
# ... 后续的分类/标签筛选逻辑 ...
# 将结果存入缓存,有效期15分钟
cache.set(cache_key, posts, 60 * 15)
# ... 分页和渲染逻辑 ...
这样,15分钟内所有对首页的访问,都将直接从内存缓存中读取数据,数据库压力骤降90%以上。
6.3 安全加固:为后台登录添加双重验证(2FA)
Django Admin是网站的“命门”,仅靠密码保护风险极高。django-otp库可以轻松为其添加短信或TOTP(基于时间的一次性密码)验证:
pip install django-otp qrcode
按照官方文档配置后,管理员登录时,除了输入密码,还需输入手机APP(如Google Authenticator)生成的6位动态验证码。这能有效阻止99%的暴力破解和密码泄露攻击。
个人体会:我在一个为客户搭建的内部知识库项目中,上线首周就遭遇了来自俄罗斯IP的暴力破解扫描。由于提前配置了
django-otp,所有攻击请求都被拦截在第二道关卡之外,后台日志里只留下了一堆无效的TOTP尝试记录。安全不是锦上添花,而是生存底线。
这个Django博客项目,远不止是一份“能跑起来的代码”。它是你理解Web开发底层逻辑的显微镜,是练习工程化思维的沙盒,更是你未来构建任何复杂应用的坚实地基。从SQLite的零配置起步,到Django Admin的声明式配置,再到MTV架构的清晰分层,每一步都经过精心设计,只为让你在亲手敲下第一个python manage.py runserver时,感受到那种“原来如此”的豁然开朗。代码的世界没有捷径,但有一个好的起点,足以让你少走三年弯路。现在,就打开终端,开始你的第一次migrate吧。
简介:直接下载就能跑的Django博客项目,用SQLite做本地数据库,不用额外装MySQL或PostgreSQL。代码里已经写好文章、分类、标签的模型结构,配好了迁移文件,执行python manage.py migrate自动建表,再用python manage.py runserver就能打开首页和后台。前台有文章列表页、详情页、按分类筛选功能;后台支持登录后发布、编辑、删除文章,管理分类和标签,所有操作都在Django admin里完成。项目包含完整的templates模板(HTML结构清晰)、static静态资源(CSS和基础JS)、urls路由配置、views视图逻辑、models数据模型和admin后台注册代码。附带README.md,一步步说明怎么安装依赖(pip install -r requirements.txt)、初始化数据库、启动服务。.idea是PyCharm配置,不影响运行,可删。适合刚学完Python想动手搭Web应用的人,边跑边理解Django的MTV流程:请求怎么从URL进来到view处理,怎么查models数据,再渲染到template展示。


被折叠的 条评论
为什么被折叠?



