很多人问过我同一个问题:Python语法学完了,下一步该干嘛?我几乎每次都给出同一个答案:去做一个属于自己的博客系统,用Django把它完完整整做出来。这不是敷衍,而是我自己的真实路径——从命令行脚本到Web全栈开发,Django博客项目是我跨过的最关键的一道坎,也是收益最明显的一次。全栈开发听着吓人,实际拆开就是"后端写接口逻辑、前端折腾页面、数据库存数据"三件事,而Django恰好把这三件事都打包好了。这篇博客会带着你从装好Python开始,一步步用手写代码的方式搭出一个能发布文章、能展示、能评论的博客,每个步骤我都会把为什么这么做的原因说清楚。不管你是刚装好Python的新手,还是已经写过一些脚本想转Web开发的同学,这套流程你都能照着抄。
1. 博客系统这一题,为什么是Django全栈开发的标准答案
在动手敲代码之前,先花几分钟把"全栈开发"这个词拆明白。很多人一听到全栈,脑子里浮现的是前端、后端、数据库、服务器一堆名词叠在一起,觉得自己得先成为半个前端专家才配谈Web开发。实际不是这么回事。
1.1 全栈开发拆开看:一个请求在Django里是怎么走完的
全栈开发说到底就是:你写的东西要能在浏览器里被看见,并且能跟数据库打交道。单机脚本学得再好,只要没有"用户通过网络访问"这一步,就不叫Web开发。Django作为Python生态里最成熟的全栈框架,把这条链路包装得非常顺滑。
在Django里有套著名的MVT思路:Model负责跟数据库对话,Template负责渲染出HTML,View负责两者之间的调度。一次完整的请求大概是这样的——你在浏览器里输入/admin/,Django先通过URLconf找到对应的视图函数,视图函数调用Model从数据库里取数据,然后把数据连同Template一起打包成HTML响应返回给浏览器。你看到的不是静态页面,而是"代码+数据"实时拼出来的内容。
这套分工和常见的MVC很像,我个人的理解是Django把Controller的角色也塞进了View里,让初学者少理解一个概念。对新手来说最重要的是别被概念绕晕,记住"URL找到函数,函数取数据,模板做展示"这个口诀就行。博客系统恰好覆盖了这条链路的每一个环节:文章列表页考验Model的查询和Template的循环渲染,详情页考验URL动态参数和异常处理,后台管理考验CRUD和权限控制,而评论功能则把表单、用户系统和数据关联一次考到位。
1.2 一个博客系统的功能清单
我建议所有第一次尝试Django的人,项目都从博客开始,而不是从后台管理系统或电商项目开始。原因很简单:博客的需求你是最清楚的,就算没人告诉你功能定义,你也知道"能发文章、能看文章、能评论"这三件事。需求描述得越清楚,踩坑时越不容易把锅甩给"需求不理解"。
做一个最基础的博客系统,需要覆盖的功能有这些:
- 文章列表页:按时间倒序展示所有已发布的文章。
- 文章详情页:展示单篇文章正文,URL里带文章ID。
- 分类功能:文章按分类归类,分类页能列出该分类下所有文章。
- 用户注册和登录:普通用户能注册登录,登录后能发文章。
- 文章发布表单:登录用户通过表单创建新文章。
- 评论功能:访问者在文章详情页提交评论,评论关联到对应文章。
- 后台管理:用Django自带的Admin管理文章、分类和评论。
你可能注意到这里面没有标签、搜索、RSS、Markdown编辑器这些东西。这很正常,我刻意把范围控制在了"刚好能跑通,又不至于做不完"的程度。扩展点文章中段会提到,等你把这个基础版做扎实了,后面往任何一个方向加深都是顺手的事。
提示:如果你之前完全没接触过Web框架,建议先把上面这张功能清单截图或者抄在纸上,每做完一个功能就在后面打个勾。这个项目做完,你对"一个网站是怎么被开发出来的"会有非常具体的体感。
2. 开工前把环境收拾利索:Python安装到Django项目骨架
万事开头难,对Python新手而言,最劝退的不是代码,而是环境安装。哪怕你已经装好了Python,我也建议你花几分钟按下面的方式确认一遍,版本不对后面Django会给你脸色看。
2.1 先从Python安装说起:PATH这件事还真别轻视
Django是一个第三方库,跑在Python之上,所以第一步是有一个可用的Python解释器。如果你用的是Windows,直接去python.org下载安装包,版本建议选3.10或3.11,这两个版本的第三方库兼容性最好,新项目用3.12也行,但没必要为了追新给自己添麻烦。
安装过程中最容易被跳过的一步,是那个"Add python.exe to PATH"复选框。很多人没勾,装完了在cmd里敲python,系统提示不是内部或外部命令。这时候不要慌,有两个解决办法。第一个是重装一遍,在安装向导第一页把Add to PATH勾上;第二个是手动配置环境变量,在系统环境变量的Path里把Python安装目录和Scripts子目录加进去,Windows用户按"此电脑→属性→高级系统设置→环境变量"的路径就能找到入口。
装好后打开cmd输入:
python --version能输出版本号,说明解释器没问题。接下来建议顺手把包管理工具pip也确认一下:
pip --version如果pip不见了,通常是Scripts目录没在Path里,用上面同样的方式补上。很多人在这步就开始打退堂鼓,其实不用,环境问题人人都会遇到一次,处理一次以后就通了。
2.2 为什么每个项目都要一个"独立小房间":虚拟环境
Python开发里有一句老话:依赖冲突比业务bug更让人头疼。假设你机器上有两个项目,一个用Django 3.2,另一个用Django 4.2,pip默认会把所有包装进同一个全局环境,你装一个卸一个,双方迟早打起来。
虚拟环境就是为了解决这个问题出现的。它相当于给每个项目单独划了一间小房间,项目A装什么库都不影响项目B。创建命令很简单:
python -m venv venvWindows下激活虚拟环境:
venv\Scripts\activatemacOS和Linux下是:
source venv/bin/activate激活后命令行前缀会出现(venv)字样,这时再用pip安装的所有包都会装进这个房间。之后安装Django:
pip install django==4.2我建议锁定4.2这个版本,它是当前的长期支持版本,官方维护周期长,很多第三方插件也为它做了适配。如果你装的时候已经出了更新的LTS版本,用新的也可以,但别随手装一个latest的dev版本,没必要拿自己的学习项目试错。
2.3 startproject和startapp:Django自动生成的文件先混个脸熟
环境就绪后,找一个干净的工作目录,执行:
django-admin startproject blog_site .注意后面那个点,它表示在当前目录生成项目,不额外新建一层文件夹。然后创建博客应用:
python manage.py startapp posts一个Django项目和一个应用的区别,你暂时可以这样理解:项目是整个网站的配置中心,应用是这个网站里的一个功能模块。博客内容都放在posts这个app里。
项目生成后,你会看到一堆文件,新手容易发懵。我把关键文件的职责理一下:
- manage.py:项目管理入口,几乎所有Django命令都从它发起。
- settings.py:项目配置文件,数据库、app注册、静态文件路径都在这里。
- urls.py:全局URL路由表。
- posts/models.py:数据模型定义的地方。
- posts/views.py:视图函数定义的地方。
- posts/admin.py:把模型注册进后台管理的入口。
在settings.py中找到INSTALLED_APPS,把posts加进去,顺便把语言和时区改成中文和上海,不然后台会显示英文而且时间会差8个小时:
INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'posts', ] LANGUAGE_CODE = 'zh-hans' TIME_ZONE = 'Asia/Shanghai'保存后运行:
python manage.py runserver浏览器访问127.0.0.1:8000,看到Django的火箭欢迎页,说明你的第一个Django项目已经从想象变成了能跑的程序。到这里,环境这关就算过了一半。
注意:runserver是开发服务器,只适合本地调试。别拿它当成正式环境一直开着用,第6章会说正式部署应该怎么做。
3. 内容为王:用ORM设计文章模型,顺手激活后台
博客系统最核心的资产是"文章"。在数据库里,文章对应一张表,表的字段就是文章的属性:标题、正文、作者、创建时间、分类等。在Django里,你不直接写SQL建表,而是写一个Python类,这就是ORM(对象关系映射)——用代码描述数据结构,数据库具体怎么建,Django帮你翻译。
3.1 用Python类定义Post和Category模型
打开posts/models.py,先定义分类模型,再定义文章模型:
from django.db import models from django.contrib.auth.models import User class Category(models.Model): name = models.CharField(max_length=50) slug = models.SlugField(unique=True, blank=True) class Meta: verbose_name = '分类' verbose_name_plural = '分类' def __str__(self): return self.name class Post(models.Model): title = models.CharField(max_length=200) slug = models.SlugField(unique=True, blank=True) category = models.ForeignKey(Category, on_delete=models.CASCADE) author = models.ForeignKey(User, on_delete=models.CASCADE) content = models.TextField() created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: ordering = ['-created_at'] verbose_name = '文章' verbose_name_plural = '文章' def __str__(self): return self.title字段的选择我一个个说清楚。CharField表示定长文本,标题设200长度是因为过长的标题本身也没意义,还能顺便限制数据库空间;TextField表示不定长文本,正文没有上限,写多少都行;SlugField是URL用的短标识,比如一篇标题为"Hello Django"的文章,slug可以自动生成为"hello-django",这样详情页URL就是/posts/hello-django/而不是/posts/3/,对搜索引擎更友好。ForeignKey是外键,它表达"多对一"关系:一篇文章属于一个分类,一个分类下面有多篇文章。on_delete=models.CASCADE的意思是,如果删了某个分类,这个分类下的文章也会一起被删,要注意这个行为是否符合你的预期。
author直接用Django内置的User模型,这样文章和注册用户就能挂上钩,后面发布文章的"作者"就可以从当前登录用户里取了。
3.2 迁移过程:makemigrations和migrate到底干了什么
模型写完后,数据库里还没有表。需要两步让它落地:
python manage.py makemigrations posts python manage.py migrate第一步makemigrations会检查模型文件,生成一个迁移文件,这个文件记录的是"表的未来样貌",可以理解成数据库的版本更新说明。第二步migrate才真正去执行这些更新,在数据库里建出表来。项目默认用的数据库是SQLite,就是一个单文件数据库,开发学习阶段完全够用,不需要额外安装数据库服务。
开发过程中模型会经常改,每次改完都要重复这两步。Django会把历史迁移记录下来,所以你不用担心改乱,如果想看某次迁移实际执行的SQL,可以运行:
python manage.py sqlmigrate posts 0001看到的是标准SQL语句,你可以在心里把ORM和SQL对应起来。这一步过了,你对"Django帮我把Python代码翻译成了建表语句"就有了直观印象。
3.3 白送的资产管理后台:Admin
接下来是最让新手兴奋的部分。Django自带一个完整的管理后台,运行:
python manage.py createsuperuser按提示输入用户名、邮箱和密码,然后在admin.py里注册模型:
from django.contrib import admin from .models import Post, Category admin.site.register(Post) admin.site.register(Category)再次运行runserver,访问127.0.0.1:8000/admin/,用刚才创建的超级用户登录,你会发现一个像模像样的后台界面。你可以在这里新增分类、写第一篇文章、编辑正文、指定作者和分类。后台里每个页面的增删改查都是Django根据你的模型自动生成的,你没写一行SQL,也没写一个增删改查函数。
到这里,博客的"内容存储"环节已经通了。虽然前台还没什么可看的,但你已经可以用后台录入数据了——这一步做好了,后续前台页面能展示什么,很大程度上就取决于你录入了什么。
提示:很多程序员看不起自带的Admin,觉得Low,但对内容型网站来说,Admin是真正的效率神器。正式项目里就算不用它做业务后台,留给运营做数据维护也是极好的。
4. 让文章见光:URL分发、视图函数与模板渲染
后台能录数据了,接下来就是把它在浏览器里展示出来。这个环节需要三兄弟配合:urls.py负责指路,views.py负责取数据,模板负责画页面。
4.1 URLconf:网站的"寻址地图"
打开项目的urls.py,把全局路由指向posts应用:
from django.contrib import admin from django.urls import path, include urlpatterns = [ path('admin/', admin.site.urls), path('posts/', include('posts.urls')), ]Django会把所有以posts/开头的请求都交给posts这个应用自己处理。接下来在posts目录下新建urls.py:
from django.urls import path from . import views urlpatterns = [ path('', views.post_list, name='post_list'), path('<int:pk>/', views.post_detail, name='post_detail'), ]这里 int:pk 是路径参数,尖括号里的内容会被解析成变量传给视图函数,int表示这个变量必须是数字。这样设计URL的好处是语义清晰,/posts/1/一看就知道是第1篇文章,这种可读性对用户和搜索引擎都有价值。
4.2 视图函数:从QuerySet到HTML
在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): post_list_all = Post.objects.all() paginator = Paginator(post_list_all, 5) page_number = request.GET.get('page') posts = paginator.get_page(page_number) return render(request, 'posts/post_list.html', {'posts': posts}) def post_detail(request, pk): post = get_object_or_404(Post, pk=pk) comments = post.comments.all() return render(request, 'posts/post_detail.html', {'post': post, 'comments': comments})Post.objects.all()这一句,返回的是QuerySet,你可以把它理解成一个"懒加载"的列表——它不会立刻把数据库所有记录都捞出来,而是等真正需要时才执行查询。这正是Django ORM舒服的地方,链式方法比如.filter(category__name='python')、.order_by('-created_at')可以任意组合,最终只在需要时拼成一条SQL执行。
列表页我加了Paginator分页,每页5篇。文章多了以后,分页是一个成熟博客的必要体验,否则一个页面加载几十篇文章,用户滚动鼠标都要滚半天。分页数据在模板里可以直接用posts.has_previous、posts.next_page_number这些属性和方法。
详情页用了get_object_or_404,它的作用是:如果这篇文章不存在,直接返回404页面,而不是抛一个异常导致页面白屏。这是Django提供的标准写法,比自己try/except更省心。
4.3 模板:{{ }}里的秘密,继承和循环
模板是Django把数据显示成HTML的地方。项目根目录下新建templates文件夹,然后在settings.py里配置:
TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [BASE_DIR / 'templates'], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ]Django查找模板时按目录层级从上往下找,为了让不同app的模板不互相覆盖,推荐在templates下再建posts子目录,对应app名。我一般把基础页做成base.html,其他页面继承它:
<!-- templates/base.html --> <!DOCTYPE html> <html lang="zh-hans"> <head> <meta charset="UTF-8"> <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"> <a class="navbar-brand" href="{% url 'post_list' %}">我的博客</a> </nav> <div class="container mt-4"> {% block content %}{% endblock %} </div> </body> </html>列表页posts/post_list.html继承base.html:
{% extends "base.html" %} {% block content %} <h1>全部文章</h1> {% for post in posts %} <article class="card mb-3"> <div class="card-body"> <h2><a href="{% url 'post_detail' pk=post.pk %}">{{ post.title }}</a></h2> <p class="text-muted">{{ post.created_at|date:"Y-m-d" }} · {{ post.author.username }}</p> <p>{{ post.content|truncatechars:100 }}</p> </div> </article> {% endfor %} {% endblock %}注意三个核心语法:{{ post.title }}输出变量,{% for post in posts %}循环,{% url 'post_detail' pk=post.pk %}反向解析出URL——它的好处是以后URL规则变了,模板不用跟着改。我用Bootstrap的CDN来快速美化页面,不用费劲手写CSS,对初学者来说这是性价比最高的选择。先把逻辑跑通,样式以后想换随时能换。
5. 登录取权:用户认证、发布表单和评论交互
一个只能看但是不能写的博客不算完整。这一节把用户注册登录、文章发布和评论串起来,让博客从"只读"变成"可写"。
5.1 Django自带的用户体系,别重复造轮子
Django的django.contrib.auth模块已经把用户表、会话、登录状态管理全都做好了。注册和登录的核心逻辑其实就几行:
from django.shortcuts import render, redirect from django.contrib.auth.forms import UserCreationForm from django.contrib.auth import login, authenticate from django.contrib.auth.decorators import login_required def register(request): if request.method == 'POST': form = UserCreationForm(request.POST) if form.is_valid(): user = form.save() login(request, user) return redirect('post_list') else: form = UserCreationForm() return render(request, 'registration/register.html', {'form': form}) def user_login(request): if request.method == 'POST': username = request.POST['username'] password = request.POST['password'] user = authenticate(request, username=username, password=password) if user is not None: login(request, user) return redirect('post_list') return render(request, 'registration/login.html')UserCreationForm是Django自带的注册表单,帮你处理了密码加密、两次密码校验和用户名唯一性检查。authenticate负责校验用户名密码,login负责把登录状态写进session。登录后,模板里可以直接用request.user获取当前用户,用user.is_authenticated判断是否已登录。
"新建文章"这个页面必须登录才能访问,在视图函数上加一个装饰器就行:
@login_required def post_create(request): ...没登录的用户访问会被重定向到登录页。这个安全机制从头到尾你没写一行session代码,这就是框架帮你把脏活累活干掉的典型例子。
5.2 ModelForm发布文章,CSRF那句话不能删
新建文章的表单,最省事的方式是用ModelForm。创建一个forms.py:
from django import forms from .models import Post class PostForm(forms.ModelForm): class Meta: model = Post fields = ['title', 'slug', 'category', 'content'] widgets = { 'content': forms.Textarea(attrs={'rows': 10, 'class': 'form-control'}), }它可以自动根据Post模型生成表单字段,你只需要声明表单里有哪些字段。视图处理上,要区分GET请求(显示空表单)和POST请求(处理提交):
@login_required def post_create(request): if request.method == 'POST': form = PostForm(request.POST) if form.is_valid(): post = form.save(commit=False) post.author = request.user post.save() return redirect('post_detail', pk=post.pk) else: form = PostForm() return render(request, 'posts/post_form.html', {'form': form})form.save(commit=False)的意思是先不写入数据库,把实例拿到手里,把当前登录用户赋值给author字段,再真正save。如果不这么写,作者字段会因为没有值而报错。
模板里对应表单时,有一行代码特别重要:
<form method="post"> {% csrf_token %} {{ form.as_p }} <button type="submit" class="btn btn-primary">发布</button> </form>{% csrf_token %}是Django的跨站请求伪造防护机制。简单说,如果不加这行,别人可以在别的网站上构造一个提交请求,冒充你发布内容。Django要求所有POST请求都要带一个它签发的token,模板里放了这行标签,表单提交时才会带上。所以千万不要觉得那行代码多余,删掉它Django会直接拒绝你的表单,这不是技术上的bug,而是安全设计。
发布成功后用redirect跳转到详情页,而不是render渲染页面,这符合Post/Redirect/Get模式,避免用户刷新页面时重复提交表单。
5.3 评论功能:顺便把一对多关系练熟
在models.py里加一个评论模型:
class Comment(models.Model): post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='comments') user = models.ForeignKey(User, on_delete=models.CASCADE) content = models.TextField(max_length=500) created_at = models.DateTimeField(auto_now_add=True) class Meta: ordering = ['created_at'] def __str__(self): return f'{self.user.username}: {self.content[:20]}'注意related_name='comments',它让Django自动生成一个反向管理器,通过post.comments.all()就能拿到这篇文章关联的所有评论。提交评论的视图:
def post_comment(request, pk): post = get_object_or_404(Post, pk=pk) if request.method == 'POST': content = request.POST.get('content') if content: Comment.objects.create(post=post, user=request.user, content=content) return redirect('post_detail', pk=post.pk)模板里评论列表和提交表单就不展开了,逻辑和文章列表一样。有一点必须提醒:评论内容是用户提交的文本,渲染时如果直接输出,用户提交什么HTML就可能执行什么,这是典型的XSS漏洞。Django模板默认对变量做了转义,所以{{ comment.content }}是安全的。如果你想关闭转义,或者允许某些富文本标签,那就要引入专门的HTML净化库,新手阶段别动这个念头,保持默认转义就好。
6. 本地测试与部署前检查,以及我踩过的几个坑
功能都跑通之后,离"能上线"还有一步。这一节我会先讲怎么用Django自带的测试机制给核心页面上一道保险,再列出部署前必须处理的配置项,最后把我自己当年踩过的几个坑原样交底。
6.1 用自带测试框架给博客上保险
Django的测试其实很简单,它能模拟一个客户端来发请求,验证页面返回是否正常。在posts/tests.py里写:
from django.test import TestCase from django.contrib.auth.models import User from .models import Post, Category class BlogTest(TestCase): def setUp(self): self.user = User.objects.create_user('admin', 'admin@example.com', 'password123') self.category = Category.objects.create(name='Python', slug='python') self.post = Post.objects.create( title='第一篇文章', slug='first-post', category=self.category, author=self.user, content='这是正文' ) def test_post_list_page(self): response = self.client.get('/posts/') self.assertEqual(response.status_code, 200) def test_post_detail_page(self): response = self.client.get(f'/posts/{self.post.pk}/') self.assertEqual(response.status_code, 200) self.assertContains(response, '第一篇文章')运行:
python manage.py test如果所有测试都通过,你会看到OK。这有什么用?以后你改动模型、调整URL、换了模板,跑一次测试就能确认核心页面没有被打挂。我在做正式项目时习惯给每个核心功能都配上这种冒烟测试,改动代码时心理负担会小很多。
6.2 上线前的配置检查清单
本地开发爽了,但不能直接拿开发配置上线。我整理了一份部署前必须过一遍的检查项:
| 检查项 | 开发环境 | 上线要求 |
|---|---|---|
| DEBUG | True | False |
| ALLOWED_HOSTS | 空列表 | 填你的域名或服务器IP |
| SECRET_KEY | 默认生成的 | 换成高强度随机串,别提交到版本库 |
| 数据库 | SQLite | 建议换PostgreSQL |
| 静态文件 | 开发服务器自动处理 | collectstatic收集到独立目录,Nginx托管 |
| 服务器 | runserver | Gunicorn等WSGI服务器 + Nginx反向代理 |
先把settings.py里的DEBUG改成False,再补上ALLOWED_HOSTS,然后运行:
python manage.py collectstatic这条命令会把所有静态文件收集到STATIC_ROOT指定的目录,交给Nginx处理。数据库在生产环境换成PostgreSQL后,settings.py里的DATABASES配置要跟着改,Django的ORM在这里展现了巨大的好处——你业务代码基本不用动,只改配置就能换数据库。
部署具体流程每家服务器厂商略有差异,但思路一致:把代码传到服务器,装Python和依赖,用Gunicorn启动Django,再用Nginx做反向代理把外部请求转发给Gunicorn,静态文件由Nginx直接托管。这个链路第一次配置可能要折腾一天,但配好一次,后面的项目就是复制粘贴。
6.3 新手最容易踩的坑,我挨个给你排一遍
最后这部分算是我当年从入门到略懂状态交的学费,每一条都是真实遇到过的。
第一个是模板路径找不到。Django的模板查找有既定目录,我一开始把post_list.html直接丢进templates根目录,然后又建了templates/posts/目录,结果视图里写posts/post_list.html时日志报TemplateDoesNotExist。排查思路很简单:看Template引擎有没有把templates目录加进搜索路径,看文件路径和视图里写的字符串是否完全一致,包括大小写和斜杠。
第二个是静态文件404。页面样式突然没了,检查请求发现静态文件全部404。开发环境里staticfiles app会帮忙处理静态文件,但前提是模板里要写{% load static %}并正确引用,配置里还要设置STATIC_URL。有时候这个问题不是配置问题,而是浏览器缓存,强制刷新一次就好了。
第三个是时间差8小时。日志里文章的创建时间比实际晚了8小时,这就是我没第一时间设置TIME_ZONE导致的。Django里USE_TZ=True的情况下,数据库存的是UTC时间,展示时要按TIME_ZONE转换成当地时区。在settings.py里把TIME_ZONE设成'Asia/Shanghai'、LANGUAGE_CODE设成'zh-hans',这类问题基本就消除了。
第四个是迁移时提示字段冲突或者关系错误。这通常发生在改了模型里的ForeignKey之后没重新生成迁移。记住一个原则:改模型后先makemigrations,再migrate,两步缺一不可。如果已经有数据但新迁移又加了非空字段,Django会让你提供默认值,这时候要么给字段设default,要么把现有数据处理好再做迁移。
第五个是中文乱码。这个现在基本不会出现,因为默认数据库是UTF-8,但如果你把数据库从SQLite换到别的系统时没注意字符集,就可能出现。建库时统一UTF-8编码能从源头避开这个坑。
这几个坑有一个共同点:都不是代码逻辑难,而是"环境和服务器的默认行为"没搞懂。遇到问题先看报错信息和日志,日志里90%的情况会直接告诉你原因,剩下10%再考虑是不是包版本冲突。
到这里,一个带文章管理、用户认证、文章发布、评论交互的Django博客系统从零到一跑通了。我当年做这个项目时,最大的收获不是会用Django,而是第一次理解了一个网站的全貌:数据怎么存、页面怎么出、用户怎么登录、请求怎么流转。回头看整个过程,卡壳最久的往往都不是框架本身,而是环境、路径、配置这类"小事",所以我在上面把这些坑都提前给了你。这个项目做完之后,你可以顺理成章地向几个方向继续走:把正文改成Markdown渲染并用highlight.js高亮代码、加一个基于关键词的全文搜索、输出RSS订阅、给评论加分页和敏感词过滤,或者把它真正部署上线,给朋友留下一个属于自己的域名。每个方向都不算难,但每个方向都会逼你学到新东西。