Django
Django 是一个高级的 Python Web 框架,鼓励快速开发和简洁实用的设计。它遵循 MTV(Model-Template-View)架构模式,提供了完整的 Web 开发解决方案。
简介
Django 特性
Django 核心特性:
- 全栈框架:提供从数据库到模板的完整解决方案
- MTV 架构:清晰的 Model-Template-View 分离
- ORM 系统:强大的数据库抽象层
- 自动化 Admin:基于模型自动生成管理后台
- 内置认证系统:用户认证、权限、分组
- 安全性高:CSRF 防护、SQL 注入防护、XSS 防护
- 可扩展性强:丰富的第三方应用
- batteries included:开箱即用的功能
适用场景:
- 大型企业级应用
- 内容管理系统(CMS)
- 电商网站
- 社交网络
- 需要 Admin 后台的项目
- 快速开发 MVP
安装 Django
# 创建虚拟环境
python -m venv venv
# Windows 激活
venv\Scripts\activate
# Linux/Mac 激活
source venv/bin/activate
# 安装 Django
pip install django
# 安装特定版本
pip install django==4.2.0
# 安装常用扩展
pip install djangorestframework # DRF: Django REST framework
pip django-cors-headers # CORS 支持
pip django-filter # 查询过滤器
pip pillow # 图片处理
# 查看 Django 版本
python -m django --version
# 或
python -c "import django; print(django.VERSION)"
快速开始
创建项目
# 创建项目
django-admin startproject myproject
# 项目结构
"""
myproject/
├── manage.py # 命令行工具
├── myproject/
│ ├── __init__.py
│ ├── settings.py # 项目配置
│ ├── urls.py # 主 URL 配置
│ ├── asgi.py # ASGI 配置
│ └── wsgi.py # WSGI 配置
"""
# 运行开发服务器
cd myproject
python manage.py runserver
# 指定端口
python manage.py runserver 8000
# 监听所有 IP
python manage.py runserver 0.0.0.0:8000
创建应用
# 创建应用
python manage.py startapp myapp
# 应用结构
"""
myapp/
├── __init__.py
├── admin.py # Admin 配置
├── apps.py # 应用配置
├── migrations/ # 数据库迁移文件
├── models.py # 数据模型
├── tests.py # 测试
├── views.py # 视图
└── urls.py # URL 配置(需要创建)
"""
# 注册应用到 settings.py
"""
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'myapp', # 添加自己的应用
]
"""
# 创建迁移
python manage.py makemigrations
# 执行迁移
python manage.py migrate
# 创建超级用户
python manage.py createsuperuser
# 启动 Shell
python manage.py shell
# 收集静态文件
python manage.py collectstatic
项目配置
# myproject/settings.py
import os
from pathlib import Path
# 基础路径
BASE_DIR = Path(__file__).resolve().parent.parent
# 密钥配置
SECRET_KEY = 'django-insecure-development-key'
# 生产环境应从环境变量读取
# SECRET_KEY = os.getenv('DJANGO_SECRET_KEY')
# 调试模式
DEBUG = True
# 允许的主机
ALLOWED_HOSTS = ['localhost', '127.0.0.1']
# 应用定义
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# 第三方应用
'rest_framework',
'corsheaders',
# 自己的应用
'myapp',
]
# 中间件
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'corsheaders.middleware.CorsMiddleware', # CORS
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
# 根 URL 配置
ROOT_URLCONF = 'myproject.urls'
# 模板配置
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',
],
},
},
]
# WSGI 配置
WSGI_APPLICATION = 'myproject.wsgi.application'
# 数据库配置
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
# PostgreSQL 配置
# 'default': {
# 'ENGINE': 'django.db.backends.postgresql',
# 'NAME': 'mydb',
# 'USER': 'user',
# 'PASSWORD': 'password',
# 'HOST': 'localhost',
# 'PORT': '5432',
# }
}
# 密码验证
AUTH_PASSWORD_VALIDATORS = [
{'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'},
{'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator'},
{'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'},
{'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'},
]
# 国际化
LANGUAGE_CODE = 'zh-hans'
TIME_ZONE = 'Asia/Shanghai'
USE_I18N = True
USE_TZ = True
# 静态文件
STATIC_URL = 'static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'
STATICFILES_DIRS = [BASE_DIR / 'static']
# 媒体文件
MEDIA_URL = 'media/'
MEDIA_ROOT = BASE_DIR / 'media'
# 默认主键类型
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
# 自定义用户模型(可选)
# AUTH_USER_MODEL = 'myapp.CustomUser'
MTV 架构
MTV 概念
"""
Django 的 MTV 架构:
M (Model): 负责数据存取层
- 定义数据结构
- 数据库操作
- 业务逻辑
T (Template): 负责表现层
- HTML 模板
- 页面展示
- 用户界面
V (View): 负责业务逻辑层
- 处理请求
- 调用 Model
- 选择 Template
- 返回响应
请求流程:
1. 用户发送 HTTP 请求
2. URL 分发找到对应的 View
3. View 处理请求,调用 Model
4. Model 返回数据
5. View 渲染 Template
6. 返回 HTTP 响应
"""
Models
基本模型定义
# myapp/models.py
from django.db import models
from django.contrib.auth.models import User
class Category(models.Model):
"""分类模型"""
name = models.CharField(max_length=100, unique=True, verbose_name='分类名')
description = models.TextField(blank=True, verbose_name='描述')
created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
updated_at = models.DateTimeField(auto_now=True, verbose_name='更新时间')
class Meta:
verbose_name = '分类'
verbose_name_plural = '分类'
ordering = ['-created_at']
def __str__(self):
return self.name
class Tag(models.Model):
"""标签模型"""
name = models.CharField(max_length=50, unique=True)
color = models.CharField(max_length=7, default='#007bff')
def __str__(self):
return self.name
class Article(models.Model):
"""文章模型"""
# 状态选项
STATUS_CHOICES = [
('draft', '草稿'),
('published', '已发布'),
('archived', '已归档'),
]
# 基本字段
title = models.CharField(max_length=200, verbose_name='标题')
slug = models.SlugField(unique=True, verbose_name='URL 别名')
content = models.TextField(verbose_name='内容')
excerpt = models.TextField(blank=True, verbose_name='摘要')
# 状态和分类
status = models.CharField(
max_length=20,
choices=STATUS_CHOICES,
default='draft',
verbose_name='状态'
)
# 关联字段
category = models.ForeignKey(
Category,
on_delete=models.CASCADE,
related_name='articles',
verbose_name='分类'
)
tags = models.ManyToManyField(
Tag,
related_name='articles',
blank=True,
verbose_name='标签'
)
author = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name='articles',
verbose_name='作者'
)
# 时间字段
created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
updated_at = models.DateTimeField(auto_now=True, verbose_name='更新时间')
published_at = models.DateTimeField(null=True, blank=True, verbose_name='发布时间')
# 统计字段
views = models.PositiveIntegerField(default=0, verbose_name='浏览次数')
likes = models.PositiveIntegerField(default=0, verbose_name='点赞数')
# 布尔字段
is_featured = models.BooleanField(default=False, verbose_name='是否精选')
allow_comments = models.BooleanField(default=True, verbose_name='允许评论')
class Meta:
verbose_name = '文章'
verbose_name_plural = '文章'
ordering = ['-created_at']
indexes = [
models.Index(fields=['-created_at']),
models.Index(fields=['status']),
]
def __str__(self):
return self.title
def get_absolute_url(self):
from django.urls import reverse
return reverse('article-detail', kwargs={'slug': self.slug})
字段类型
from django.db import models
class FieldTypes(models.Model):
"""字段类型示例"""
# 字符串字段
char_field = models.CharField(max_length=100) # 固定长度字符串
text_field = models.TextField() # 大文本
slug_field = models.SlugField() # URL 别名
email_field = models.EmailField() # 邮箱
url_field = models.URLField() # URL
ip_field = models.GenericIPAddressField() # IP 地址
# 数字字段
integer_field = models.IntegerField() # 整数
small_integer = models.SmallIntegerField() # 小整数
big_integer = models.BigIntegerField() # 大整数
positive_integer = models.PositiveIntegerField() # 正整数
float_field = models.FloatField() # 浮点数
decimal_field = models.DecimalField(max_digits=10, decimal_places=2) # 精确小数
# 日期时间
date_field = models.DateField() # 日期
time_field = models.TimeField() # 时间
datetime_field = models.DateTimeField() # 日期时间
duration = models.DurationField() # 时间段
# 布尔字段
boolean_field = models.BooleanField() # 布尔值
# 文件字段
file_field = models.FileField(upload_to='files/') # 文件
image_field = models.ImageField(upload_to='images/') # 图片
# JSON 字段(Django 3.2+)
json_field = models.JSONField() # JSON 数据
# 二进制
binary_field = models.BinaryField() # 二进制数据
# UUID
uuid_field = models.UUIDField() # UUID
# 字段参数
class FieldOptions(models.Model):
"""字段参数示例"""
# null:数据库字段是否可为 NULL
field1 = models.CharField(max_length=100, null=True)
# blank:表单验证时是否可为空
field2 = models.CharField(max_length=100, blank=True)
# default:默认值
field3 = models.IntegerField(default=0)
# unique:唯一约束
field4 = models.CharField(max_length=100, unique=True)
# primary_key:主键
field5 = models.AutoField(primary_key=True)
# choices:选择项
STATUS_CHOICES = [
(1, '激活'),
(0, '未激活'),
]
field6 = models.IntegerField(choices=STATUS_CHOICES)
# verbose_name:别名
field7 = models.CharField(max_length=100, verbose_name='名称')
# help_text:帮助文本
field8 = models.CharField(max_length=100, help_text='请输入名称')
# editable:是否可编辑
field9 = models.CharField(max_length=100, editable=False)
# auto_now:自动更新为当前时间
field10 = models.DateTimeField(auto_now=True)
# auto_now_add:创建时自动设置为当前时间
field11 = models.DateTimeField(auto_now_add=True)
# db_index:数据库索引
field12 = models.CharField(max_length=100, db_index=True)
关系字段
from django.db import models
class Author(models.Model):
name = models.CharField(max_length=100)
email = models.EmailField()
def __str__(self):
return self.name
class Publisher(models.Model):
name = models.CharField(max_length=100)
address = models.CharField(max_length=200)
def __str__(self):
return self.name
class Book(models.Model):
title = models.CharField(max_length=200)
author = models.ForeignKey(
Author,
on_delete=models.CASCADE, # 删除作者时删除书籍
related_name='books', # 反向关系名称
verbose_name='作者'
)
publisher = models.ForeignKey(
Publisher,
on_delete=models.SET_NULL, # 删除出版社时设为 NULL
null=True,
blank=True
)
co_authors = models.ManyToManyField(
Author,
related_name='co_authored_books',
blank=True,
verbose_name='合著者'
)
# on_delete 选项
"""
CASCADE:级联删除,删除关联对象时也删除该对象
PROTECT:保护,删除关联对象时抛出 ProtectedError
SET_NULL:设为 NULL,需要字段 null=True
SET_DEFAULT:设为默认值,需要字段有 default 值
SET():设为指定值
DO_NOTHING:什么都不做
"""
# 查询关系
# 正向查询
book = Book.objects.first()
author = book.author
co_authors = book.co_authors.all()
# 反向查询
author = Author.objects.first()
books = author.books.all() # 使用 related_name
co_authored = author.co_authored_books.all()
# 跨关系查询
# 查询作者名为 "John" 的所有书籍
books = Book.objects.filter(author__name='John')
# 查询出版社地址包含 "Beijing" 的书籍
books = Book.objects.filter(publisher__address__contains='Beijing')
Meta 选项
from django.db import models
class MetaOptions(models.Model):
title = models.CharField(max_length=200)
created_at = models.DateTimeField(auto_now_add=True)
status = models.CharField(max_length=20)
order = models.IntegerField()
class Meta:
# 表名
db_table = 'custom_table_name'
# 管理后台显示名称
verbose_name = '元选项'
verbose_name_plural = '元选项列表'
# 排序
ordering = ['-created_at']
# 联合唯一约束
unique_together = [['title', 'status']]
# 联合索引
indexes = [
models.Index(fields=['title', 'status']),
models.Index(fields=['-created_at']),
]
# 抽象基类,不创建表
abstract = True
# 权限
permissions = [
('can_view', 'Can view'),
('can_edit', 'Can edit'),
]
# 默认管理器
default_manager_name = 'objects'
# 是否获取数据时使用默认排序
get_latest_by = 'created_at'
# 抽象基类
class TimeStampedModel(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
abstract = True # 抽象类,不会创建表
class Article(TimeStampedModel):
title = models.CharField(max_length=200)
# 继承 created_at 和 updated_at 字段
数据库迁移
# 生成迁移文件
python manage.py makemigrations
# 为指定应用生成迁移
python manage.py makemigrations myapp
# 查看迁移 SQL
python manage.py sqlmigrate myapp 0001
# 执行迁移
python manage.py migrate
# 回滚迁移
python manage.py migrate myapp 0001
# 查看迁移状态
python manage.py showmigrations
# 合并迁移
python manage.py mergemigrations
# 重置迁移(危险操作)
python manage.py migrate myapp zero
# 迁移文件示例
# Generated by Django X.X
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0012_...'),
]
operations = [
migrations.CreateModel(
name='Article',
fields=[
('id', models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name='ID'
)),
('title', models.CharField(max_length=200, verbose_name='标题')),
('content', models.TextField(verbose_name='内容')),
('author', models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name='articles',
to='auth.user'
)),
],
options={
'verbose_name': '文章',
'verbose_name_plural': '文章',
},
),
]
Views & URLs
函数视图
# myapp/views.py
from django.http import HttpResponse, JsonResponse, Http404
from django.shortcuts import render, get_object_or_404, redirect
from django.views.decorators.http import require_http_methods
from django.views.decorators.csrf import csrf_exempt
from .models import Article
# 简单视图
def index(request):
return HttpResponse('Hello, Django!')
# 渲染模板
def article_list(request):
articles = Article.objects.filter(status='published')
context = {
'articles': articles,
'title': '文章列表'
}
return render(request, 'myapp/article_list.html', context)
# 获取单个对象
def article_detail(request, pk):
# 方式1: 手动获取并处理 404
try:
article = Article.objects.get(pk=pk)
except Article.DoesNotExist:
raise Http404('文章不存在')
# 方式2: 使用快捷函数
article = get_object_or_404(Article, pk=pk)
context = {'article': article}
return render(request, 'myapp/article_detail.html', context)
# JSON 响应
def api_articles(request):
articles = Article.objects.all()
data = [
{
'id': article.id,
'title': article.title,
'author': article.author.username
}
for article in articles
]
return JsonResponse(data, safe=False)
# 重定向
def redirect_to_index(request):
return redirect('index')
# 处理表单
@require_http_methods(['GET', 'POST'])
def article_create(request):
if request.method == 'POST':
title = request.POST.get('title')
content = request.POST.get('content')
article = Article.objects.create(
title=title,
content=content,
author=request.user
)
return redirect('article-detail', pk=article.pk)
return render(request, 'myapp/article_form.html')
# URL 参数传递
def article_by_slug(request, slug):
article = get_object_or_404(Article, slug=slug)
return render(request, 'myapp/article_detail.html', {'article': article})
def article_by_year(request, year):
articles = Article.objects.filter(created_at__year=year)
return render(request, 'myapp/article_list.html', {'articles': articles})
# 查询字符串
def search(request):
query = request.GET.get('q', '')
articles = Article.objects.filter(title__icontains=query)
return render(request, 'myapp/search.html', {'articles': articles, 'query': query})
类视图
from django.views import View
from django.views.generic import (
ListView, DetailView, CreateView, UpdateView, DeleteView
)
from django.urls import reverse_lazy
from .models import Article
# 基础 View 类
class MyView(View):
"""基础视图类"""
def get(self, request, *args, **kwargs):
# 处理 GET 请求
return HttpResponse('GET 请求')
def post(self, request, *args, **kwargs):
# 处理 POST 请求
return HttpResponse('POST 请求')
def put(self, request, *args, **kwargs):
# 处理 PUT 请求
return HttpResponse('PUT 请求')
def delete(self, request, *args, **kwargs):
# 处理 DELETE 请求
return HttpResponse('DELETE 请求')
# ListView: 列表视图
class ArticleListView(ListView):
"""文章列表视图"""
model = Article
template_name = 'myapp/article_list.html'
context_object_name = 'articles'
paginate_by = 10 # 分页
ordering = ['-created_at']
# 额外上下文
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = '文章列表'
return context
# 自定义查询集
def get_queryset(self):
queryset = super().get_queryset()
return queryset.filter(status='published')
# DetailView: 详情视图
class ArticleDetailView(DetailView):
"""文章详情视图"""
model = Article
template_name = 'myapp/article_detail.html'
context_object_name = 'article'
slug_url_kwarg = 'slug'
slug_field = 'slug'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# 增加浏览次数
article = self.object
article.views += 1
article.save(update_fields=['views'])
return context
# CreateView: 创建视图
class ArticleCreateView(CreateView):
"""创建文章视图"""
model = Article
template_name = 'myapp/article_form.html'
fields = ['title', 'slug', 'content', 'category', 'tags']
success_url = reverse_lazy('article-list')
def form_valid(self, form):
# 设置作者
form.instance.author = self.request.user
return super().form_valid(form)
# UpdateView: 更新视图
class ArticleUpdateView(UpdateView):
"""更新文章视图"""
model = Article
template_name = 'myapp/article_form.html'
fields = ['title', 'slug', 'content', 'category', 'tags']
def get_success_url(self):
return reverse_lazy('article-detail', kwargs={'pk': self.object.pk})
# DeleteView: 删除视图
class ArticleDeleteView(DeleteView):
"""删除文章视图"""
model = Article
success_url = reverse_lazy('article-list')
def post(self, request, *args, **kwargs):
# 要求确认删除
return super().post(request, *args, **kwargs)
URL 配置
# myproject/urls.py (主 URL 配置)
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('myapp.urls')), # 包含应用的 URL
path('api/', include('api.urls')), # API URL
]
# 开发环境下提供媒体文件服务
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
# myapp/urls.py (应用 URL 配置)
from django.urls import path
from . import views
app_name = 'myapp' # URL 命名空间
urlpatterns = [
# 基本路径
path('', views.index, name='index'),
# 带参数的路径
path('article/<int:pk>/', views.article_detail, name='article-detail'),
path('article/<slug:slug>/', views.article_by_slug, name='article-slug'),
# 类视图
path('articles/', views.ArticleListView.as_view(), name='article-list'),
path('article/create/', views.ArticleCreateView.as_view(), name='article-create'),
path('article/<int:pk>/update/', views.ArticleUpdateView.as_view(), name='article-update'),
path('article/<int:pk>/delete/', views.ArticleDeleteView.as_view(), name='article-delete'),
# 包含其他 URL 配置
path('api/', include('api.urls')),
]
# URL 转换器
"""
str: 匹配非空字符串,不包括路径分隔符'/'
int: 匹配非负整数
slug: 匹配由字母、数字、连字符和下划线组成的 slug
uuid: 匹配格式化的 UUID
path: 匹配非空字符串,包括路径分隔符'/'
自定义转换器:
"""
# 在 urls.py 中定义转换器
from django.urls import register_converter
class YearConverter:
regex = '[0-9]{4}'
def to_python(self, value):
return int(value)
def to_url(self, value):
return str(value)
register_converter(YearConverter, 'year')
# 使用自定义转换器
path('articles/<year:year>/', views.article_by_year)
URL 反向解析
from django.urls import reverse
from django.shortcuts import redirect
# 在视图或代码中反向解析 URL
# 不带参数
url = reverse('index') # '/'
# 带参数
url = reverse('article-detail', kwargs={'pk': 1}) # '/article/1/'
url = reverse('article-detail', args=[1]) # '/article/1/'
# 带命名空间
url = reverse('myapp:article-list') # '/articles/'
# 重定向
return redirect('article-detail', pk=1)
# 在模板中使用
"""
{# 不带参数 #}
<a href="{% url 'index' %}">首页</a>
{# 带参数 #}
<a href="{% url 'article-detail' article.pk %}">查看</a>
{# 带命名空间 #}
<a href="{% url 'myapp:article-list' %}">文章列表</a>
"""
Templates
模板语法
<!-- templates/base.html -->
<!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>
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<!-- 导航栏 -->
<nav>
<a href="{% url 'index' %}">首页</a>
<a href="{% url 'article-list' %}">文章</a>
{% if user.is_authenticated %}
<span>欢迎, {{ user.username }}</span>
<a href="{% url 'logout' %}">登出</a>
{% else %}
<a href="{% url 'login' %}">登录</a>
<a href="{% url 'register' %}">注册</a>
{% endif %}
</nav>
<!-- Flash 消息 -->
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }}">
{{ message }}
</div>
{% endfor %}
{% endif %}
<!-- 主内容 -->
<main>
{% block content %}{% endblock %}
</main>
<!-- 侧边栏 -->
<aside>
{% block sidebar %}{% endblock %}
</aside>
<!-- 页脚 -->
<footer>
{% block footer %}
<p>© 2024 我的公司</p>
{% endblock %}
</footer>
</body>
</html>
<!-- templates/myapp/article_list.html -->
{% extends "base.html" %}
{% block title %}文章列表 - 我的网站{% endblock %}
{% block content %}
<h1>文章列表</h1>
<!-- 变量 -->
<p>共 {{ articles|length }} 篇文章</p>
<!-- 过滤器 -->
<p>{{ articles.count|add:10 }}</p>
<p>{{ title|upper }}</p>
<p>{{ content|truncatewords:50 }}</p>
<p="{{ price|floatformat:2 }}</p>
<!-- 日期过滤器 -->
<p>{{ article.created_at|date:"Y-m-d H:i" }}</p>
<p="{{ article.created_at|timesince }}</p>
<!-- 条件语句 -->
{% if articles %}
<ul>
{% for article in articles %}
<li>
<h3>{{ article.title }}</h3>
<p>作者: {{ article.author.username }}</p>
<p>创建时间: {{ article.created_at|date:"Y-m-d" }}</p>
<!-- 条件 -->
{% if article.is_featured %}
<span class="badge">精选</span>
{% endif %}
{% if article.status == "published" %}
<span class="status published">已发布</span>
{% elif article.status == "draft" %}
<span class="status draft">草稿</span>
{% endif %}
<!-- URL -->
<a href="{% url 'article-detail' article.pk %}">查看详情</a>
<a href="{{ article.get_absolute_url }}">查看详情</a>
</li>
{% empty %}
<li>没有文章</li>
{% endfor %}
</ul>
{% else %}
<p>暂无文章</p>
{% endif %}
<!-- 循环变量 -->
{% for article in articles %}
{% if forloop.first %}
<p>第一个</p>
{% endif %}
<p>当前索引: {{ forloop.counter0 }}</p>
<p>总数: {{ forloop.revcounter }}</p>
{% if forloop.last %}
<p>最后一个</p>
{% endif %}
{% endfor %}
{% endblock %}
{% block sidebar %}
<h3>热门文章</h3>
{% for article in popular_articles %}
<a href="{% url 'article-detail' article.pk %}">
{{ article.title }}
</a>
{% endfor %}
{% endblock %}
自定义过滤器
# myapp/templatetags/__init__.py
# 确保这是一个包
# myapp/templatetags/custom_filters.py
from django import template
register = template.Library()
@register.filter
def truncate_chars(value, arg):
"""截断字符"""
if len(value) > arg:
return value[:arg] + '...'
return value
@register.filter
def multiply(value, arg):
"""乘法"""
return value * arg
@register.filter
def to_class_name(value):
"""获取类名"""
return value.__class__.__name__
@register.simple_tag
def current_time(format_string):
"""当前时间标签"""
from datetime import datetime
return datetime.now().strftime(format_string)
@register.inclusion_tag('myapp/pagination.html')
def pagination(page_obj):
"""分页标签"""
return {'page_obj': page_obj}
<!-- 使用自定义过滤器 -->
{% load custom_filters %}
{{ content|truncate_chars:100 }}
{{ price|multiply:2 }}
{% current_time "%Y-%m-%d %H:%M:%S" %}
Forms
Form 类
# myapp/forms.py
from django import forms
from .models import Article, Category
class ArticleForm(forms.Form):
"""文章表单"""
title = forms.CharField(
max_length=200,
label='标题',
widget=forms.TextInput(attrs={'class': 'form-control'}),
error_messages={'required': '请输入标题'}
)
content = forms.CharField(
label='内容',
widget=forms.Textarea(attrs={'rows': 10, 'class': 'form-control'}),
required=True
)
email = forms.EmailField(
label='邮箱',
required=False
)
category = forms.ModelChoiceField(
queryset=Category.objects.all(),
label='分类',
empty_label='请选择分类'
)
# 日期
publish_date = forms.DateField(
label='发布日期',
widget=forms.DateInput(attrs={'type': 'date'})
)
# 布尔
is_featured = forms.BooleanField(
label='是否精选',
required=False
)
# 多选
tags = forms.MultipleChoiceField(
choices=[('python', 'Python'), ('django', 'Django'), ('flask', 'Flask')],
label='标签',
widget=forms.CheckboxSelectMultiple
)
# 自定义验证
def clean_title(self):
title = self.cleaned_data.get('title')
if len(title) < 5:
raise forms.ValidationError('标题至少5个字符')
return title
def clean(self):
cleaned_data = super().clean()
title = cleaned_data.get('title')
content = cleaned_data.get('content')
if title and content and title in content:
raise forms.ValidationError('标题不能出现在内容中')
return cleaned_data
ModelForm
class ArticleModelForm(forms.ModelForm):
"""文章 ModelForm"""
class Meta:
model = Article
fields = ['title', 'slug', 'content', 'category', 'tags', 'is_featured']
# fields = '__all__' # 所有字段
# exclude = ['author'] # 排除字段
widgets = {
'title': forms.TextInput(attrs={'class': 'form-control'}),
'content': forms.Textarea(attrs={'rows': 10}),
'slug': forms.TextInput(attrs={'readonly': 'readonly'}),
}
labels = {
'title': '标题',
'content': '内容',
}
help_texts = {
'slug': 'URL 别名,自动生成',
}
error_messages = {
'title': {
'required': '请输入标题',
'max_length': '标题不能超过200个字符',
}
}
# 自定义验证
def clean_title(self):
title = self.cleaned_data.get('title')
# 验证逻辑
return title
# 在视图中使用 ModelForm
from django.shortcuts import render, redirect
from .forms import ArticleModelForm
def article_create(request):
if request.method == 'POST':
form = ArticleModelForm(request.POST)
if form.is_valid():
article = form.save(commit=False)
article.author = request.user
article.save()
# 保存多对多关系
form.save_m2m()
return redirect('article-detail', pk=article.pk)
else:
form = ArticleModelForm()
return render(request, 'myapp/article_form.html', {'form': form})
表单验证
# 视图中处理表单
from django.shortcuts import render, redirect
from .forms import ArticleForm
def article_create(request):
if request.method == 'POST':
form = ArticleForm(request.POST)
if form.is_valid():
# 获取验证后的数据
title = form.cleaned_data['title']
content = form.cleaned_data['content']
# 保存数据
article = Article.objects.create(
title=title,
content=content,
author=request.user
)
return redirect('article-detail', pk=article.pk)
else:
form = ArticleForm()
return render(request, 'myapp/article_form.html', {'form': form})
# 模板中渲染表单
<form method="POST">
{% csrf_token %}
<!-- 方式1: 手动渲染 -->
{{ form.as_p }}
<!-- 方式2: 手动控制 -->
<div>
<label for="{{ form.title.id_for_label }}">{{ form.title.label }}</label>
{{ form.title }}
{% if form.title.errors %}
<div class="error">{{ form.title.errors }}</div>
{% endif %}
</div>
<div>
{{ form.content.label_tag }}
{{ form.content }}
{{ form.content.help_text }}
{% if form.content.errors %}
<div class="error">{{ form.content.errors }}</div>
{% endif %}
</div>
<!-- 方式3: 循环渲染 -->
{% for field in form %}
<div>
{{ field.label_tag }}
{{ field }}
{% if field.errors %}
<div class="error">{{ field.errors }}</div>
{% endif %}
{% if field.help_text %}
<small>{{ field.help_text }}</small>
{% endif %}
</div>
{% endfor %}
<button type="submit">提交</button>
</form>
<!-- 非绑定表单(显示初始数据) -->
<form method="POST">
{% csrf_token %}
{% for field in form %}
<div>
{{ field.label_tag }}
{{ field }}
</div>
{% endfor %}
<button type="submit">提交</button>
</form>
Admin
注册模型
# myapp/admin.py
from django.contrib import admin
from .models import Article, Category, Tag
# 方式1:简单注册
admin.site.register(Article)
admin.site.register(Category)
admin.site.register(Tag)
# 方式2:使用 ModelAdmin
@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
list_display = ('title', 'category', 'author', 'status', 'views', 'created_at')
list_filter = ('status', 'category', 'created_at', 'tags')
search_fields = ('title', 'content')
list_editable = ('status', 'is_featured')
list_per_page = 20
ordering = ('-created_at',)
date_hierarchy = 'created_at'
# 字段集
fieldsets = (
('基本信息', {
'fields': ('title', 'slug', 'category', 'tags')
}),
('内容', {
'fields': ('content', 'excerpt')
}),
('设置', {
'fields': ('status', 'is_featured', 'allow_comments'),
'classes': ('collapse',) # 可折叠
}),
('统计', {
'fields': ('views', 'likes'),
'classes': ('collapse',)
}),
)
# 只读字段
readonly_fields = ('created_at', 'updated_at', 'views')
# 行内编辑
inlines = [CommentInline]
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
list_display = ('name', 'article_count', 'created_at')
prepopulated_fields = {'slug': ('name',)} # 自动填充
def article_count(self, obj):
return obj.articles.count()
article_count.short_description = '文章数'
# 行内编辑
class Comment(admin.TabularInline):
model = Comment
extra = 1
自定义 Admin
# 自定义 Admin 站点
from django.contrib.admin import AdminSite
from django.contrib.admin.sites import site
class MyAdminSite(AdminSite):
site_header = '我的管理后台'
site_title = '管理后台'
index_title = '欢迎来到管理后台'
def get_urls(self):
from django.urls import path
urls = super().get_urls()
custom_urls = [
path('my-view/', self.admin_view(my_view), name='my-view'),
]
return custom_urls + urls
def my_view(request):
return render(request, 'admin/my_view.html')
# 创建自定义 admin 站点实例
my_admin_site = MyAdminSite(name='myadmin')
my_admin_site.register(Article)
# 自定义 Admin 动作
@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
actions = ['make_published', 'make_draft']
def make_published(self, request, queryset):
updated = queryset.update(status='published')
self.message_user(request, f'{updated} 篇文章已发布')
make_published.short_description = '标记为已发布'
def make_draft(self, request, queryset):
updated = queryset.update(status='draft')
self.message_user(request, f'{updated} 篇文章已设为草稿')
make_draft.short_description = '标记为草稿'
用户认证
User 模型
# 使用 Django 内置的 User 模型
from django.contrib.auth.models import User
from django.db import models
class UserProfile(models.Model):
"""用户扩展信息"""
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
phone = models.CharField(max_length=20, blank=True)
avatar = models.ImageField(upload_to='avatars/', blank=True)
bio = models.TextField(blank=True)
def __str__(self):
return f'{self.user.username} 的资料'
# 创建自定义用户模型
from django.contrib.auth.models import AbstractUser
class CustomUser(AbstractUser):
"""自定义用户模型"""
nickname = models.CharField(max_length=100, blank=True)
phone = models.CharField(max_length=20, unique=True, null=True, blank=True)
avatar = models.ImageField(upload_to='avatars/', blank=True)
def __str__(self):
return self.username
# 在 settings.py 中指定
# AUTH_USER_MODEL = 'myapp.CustomUser'
认证系统
from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.decorators import login_required
from django.shortcuts import render, redirect
from django.contrib.auth.forms import UserCreationForm
# 登录
def user_login(request):
if request.method == 'POST':
username = request.POST.get('username')
password = request.POST.get('password')
# 验证用户
user = authenticate(request, username=username, password=password)
if user is not None:
if user.is_active:
login(request, user)
return redirect('index')
else:
return render(request, 'login.html', {'error': '账户已被禁用'})
else:
return render(request, 'login.html', {'error': '用户名或密码错误'})
return render(request, 'login.html')
# 登出
def user_logout(request):
logout(request)
return redirect('index')
# 注册
def user_register(request):
if request.method == 'POST':
form = UserCreationForm(request.POST)
if form.is_valid():
user = form.save()
login(request, user)
return redirect('index')
else:
form = UserCreationForm()
return render(request, 'register.html', {'form': form})
# 登录要求
@login_required(login_url='/login/')
def profile(request):
return render(request, 'profile.html')
# CBV 登录要求
from django.contrib.auth.mixins import LoginRequiredMixin
class ProfileView(LoginRequiredMixin, TemplateView):
template_name = 'profile.html'
login_url = '/login/'
权限控制
# 装饰器检查权限
from django.contrib.auth.decorators import permission_required, login_required
@permission_required('myapp.can_edit', raise_exception=True)
def edit_article(request, pk):
# 需要特定权限
pass
# 类视图权限
from django.contrib.auth.mixins import PermissionRequiredMixin, LoginRequiredMixin
class ArticleUpdateView(PermissionRequiredMixin, UpdateView):
model = Article
permission_required = 'myapp.can_edit'
raise_exception = True
# 在模板中检查权限
"""
{% if perms.myapp.can_edit %}
<a href="#">编辑</a>
{% endif %}
{% if user.is_superuser %}
<a href="#">管理</a>
{% endif %}
"""
# 创建权限
from django.db import models
class Article(models.Model):
# ... 字段定义 ...
class Meta:
permissions = [
('can_view', 'Can view article'),
('can_edit', 'Can edit article'),
('can_delete', 'Can delete article'),
]
# 检查用户权限
def check_permission(request):
user = request.user
# 检查是否有权限
if user.has_perm('myapp.can_edit'):
# 有权限
pass
# 检查是否是超级用户
if user.is_superuser:
pass
# 检查是否是特定组的成员
if user.groups.filter(name='Editors').exists():
pass
中间件
# 自定义中间件
class CustomMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
# 请求处理前的代码
print('Before view')
response = self.get_response(request)
# 响应处理后的代码
print('After view')
return response
# myapp/middleware.py
from django.shortcuts import redirect
class LoginRequiredMiddleware:
"""登录检查中间件"""
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
# 不需要登录的路径
exempt_urls = ['/login/', '/register/', '/static/']
if not request.user.is_authenticated and request.path not in exempt_urls:
return redirect('/login/')
return self.get_response(request)
# 在 settings.py 中注册
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'myapp.middleware.CustomMiddleware', # 自定义中间件
]
REST Framework
安装和配置
pip install djangorestframework
pip install django-filter # 过滤
pip install markdown # Markdown 支持
# settings.py
INSTALLED_APPS = [
...
'rest_framework',
'rest_framework.authtoken', # Token 认证
'django_filters',
]
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.TokenAuthentication',
'rest_framework.authentication.SessionAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 10,
'DEFAULT_FILTER_BACKENDS': [
'django_filters.rest_framework.DjangoFilterBackend',
'rest_framework.filters.SearchFilter',
'rest_framework.filters.OrderingFilter',
],
}
Serializers
# myapp/serializers.py
from rest_framework import serializers
from .models import Article, Category
class CategorySerializer(serializers.ModelSerializer):
"""分类序列化器"""
article_count = serializers.SerializerMethodField()
class Meta:
model = Category
fields = ['id', 'name', 'description', 'article_count']
def get_article_count(self, obj):
return obj.articles.count()
class ArticleSerializer(serializers.ModelSerializer):
"""文章序列化器"""
author_username = serializers.SerializerMethodField()
category_name = serializers.SerializerMethodField()
absolute_url = serializers.SerializerMethodField()
class Meta:
model = Article
fields = [
'id', 'title', 'slug', 'content', 'excerpt',
'status', 'category', 'category_name',
'author', 'author_username',
'created_at', 'updated_at',
'views', 'likes', 'is_featured',
'absolute_url'
]
read_only_fields = ['created_at', 'updated_at', 'views']
def get_author_username(self, obj):
return obj.author.username
def get_category_name(self, obj):
return obj.category.name if obj.category else None
def get_absolute_url(self, obj):
return obj.get_absolute_url()
# 验证
class ArticleCreateSerializer(serializers.ModelSerializer):
class Meta:
model = Article
fields = ['title', 'content', 'category', 'tags']
def validate_title(self, value):
if len(value) < 5:
raise serializers.ValidationError('标题至少5个字符')
return value
def validate(self, data):
title = data.get('title')
content = data.get('content')
if title and title in content:
raise serializers.ValidationError('标题不能出现在内容中')
return data
ViewSets
# myapp/views.py (API)
from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticatedOrReadOnly
from .models import Article
from .serializers import ArticleSerializer, ArticleCreateSerializer
class ArticleViewSet(viewsets.ModelViewSet):
"""文章 ViewSet"""
queryset = Article.objects.filter(status='published')
serializer_class = ArticleSerializer
permission_classes = [IsAuthenticatedOrReadOnly]
filterset_fields = ['category', 'status', 'tags']
search_fields = ['title', 'content']
ordering_fields = ['created_at', 'views', 'likes']
ordering = ['-created_at']
def get_serializer_class(self):
if self.action == 'create':
return ArticleCreateSerializer
return ArticleSerializer
def perform_create(self, serializer):
serializer.save(author=self.request.user)
# 自定义 action
@action(detail=True, methods=['post'])
def like(self, request, pk=None):
"""点赞"""
article = self.get_object()
article.likes += 1
article.save()
return Response({'likes': article.likes})
@action(detail=False, methods=['get'])
def featured(self, request):
"""精选文章"""
featured = self.queryset.filter(is_featured=True)
serializer = self.get_serializer(featured, many=True)
return Response(serializer.data)
@action(detail=True, methods=['get', 'post'])
def comments(self, request, pk=None):
"""文章评论"""
article = self.get_object()
if request.method == 'GET':
comments = article.comments.all()
# 序列化并返回
return Response({'comments': []})
elif request.method == 'POST':
# 创建评论
return Response({'message': '评论创建成功'})
Routers
# myapp/urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import ArticleViewSet
router = DefaultRouter()
router.register(r'articles', ArticleViewSet, basename='article')
urlpatterns = [
path('api/', include(router.urls)),
]
# 生成的 URL:
# GET /api/articles/ - 列表
# POST /api/articles/ - 创建
# GET /api/articles/{id}/ - 详情
# PUT /api/articles/{id}/ - 更新
# DELETE /api/articles/{id}/ - 删除
# POST /api/articles/{id}/like/ - 自定义 action
# GET /api/articles/featured/ - 自定义 action
项目结构
推荐的项目结构
myproject/
├── manage.py
├── myproject/
│ ├── __init__.py
│ ├── settings/
│ │ ├── __init__.py
│ │ ├── base.py # 基础配置
│ │ ├── development.py # 开发配置
│ │ ├── production.py # 生产配置
│ │ └── test.py # 测试配置
│ ├── urls.py
│ ├── wsgi.py
│ └── asgi.py
├── apps/
│ ├── __init__.py
│ ├── core/ # 核心应用
│ │ ├── __init__.py
│ │ ├── models.py
│ │ ├── views.py
│ │ ├── urls.py
│ │ ├── forms.py
│ │ ├── admin.py
│ │ ├── serializers.py
│ │ ├── permissions.py
│ │ ├── filters.py
│ │ └── templates/
│ │ └── core/
│ ├── users/ # 用户应用
│ └── blog/ # 博客应用
├── templates/ # 全局模板
│ ├── base.html
│ └── registration/
├── static/ # 静态文件
│ ├── css/
│ ├── js/
│ └── images/
├── media/ # 媒体文件
│ ├── uploads/
│ └── avatars/
├── locale/ # 国际化
├── requirements/ # 依赖文件
│ ├── base.txt
│ ├── development.txt
│ └── production.txt
├── tests/ # 测试
│ ├── __init__.py
│ ├── conftest.py
│ ├── test_models.py
│ ├── test_views.py
│ └── test_api.py
├── docs/ # 文档
└── scripts/ # 脚本
配置文件拆分
# settings/base.py
import os
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent.parent
SECRET_KEY = os.getenv('DJANGO_SECRET_KEY')
DEBUG = False
ALLOWED_HOSTS = []
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
]
ROOT_URLCONF = 'myproject.urls'
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',
],
},
},
]
WSGI_APPLICATION = 'myproject.wsgi.application'
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': os.getenv('DB_NAME'),
'USER': os.getenv('DB_USER'),
'PASSWORD': os.getenv('DB_PASSWORD'),
'HOST': os.getenv('DB_HOST'),
'PORT': os.getenv('DB_PORT', '5432'),
}
}
AUTH_PASSWORD_VALIDATORS = []
LANGUAGE_CODE = 'zh-hans'
TIME_ZONE = 'Asia/Shanghai'
USE_I18N = True
USE_TZ = True
STATIC_URL = 'static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'
STATICFILES_DIRS = [BASE_DIR / 'static']
MEDIA_URL = 'media/'
MEDIA_ROOT = BASE_DIR / 'media'
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
# settings/development.py
from .base import *
DEBUG = True
ALLOWED_HOSTS = ['localhost', '127.0.0.1']
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
# settings/production.py
from .base import *
DEBUG = False
ALLOWED_HOSTS = ['example.com', 'www.example.com']
# 安全设置
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
X_FRAME_OPTIONS = 'DENY'
# 使用 manage.py
import os
os.getenv('DJANGO_SETTINGS_MODULE', 'myproject.settings.development')
部署
Gunicorn + Nginx
# 安装 Gunicorn
pip install gunicorn
# 运行 Gunicorn
gunicorn myproject.wsgi:application --bind 0.0.0.0:8000
# 配置选项
gunicorn myproject.wsgi:application \
--bind 0.0.0.0:8000 \
--workers 4 \
--worker-class sync \
--worker-connections 1000 \
--max-requests 1000 \
--max-requests-jitter 50 \
--timeout 30 \
--keepalive 5 \
--access-logfile - \
--error-logfile - \
--log-level info
# /etc/nginx/sites-available/myproject
server {
listen 80;
server_name example.com;
# 静态文件
location /static/ {
alias /path/to/myproject/staticfiles/;
}
# 媒体文件
location /media/ {
alias /path/to/myproject/media/;
}
# 代理到 Gunicorn
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;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
# HTTPS 配置
server {
listen 443 ssl;
server_name example.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
# SSL 配置
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
# 其他配置同上
}
# HTTP 重定向到 HTTPS
server {
listen 80;
server_name example.com;
return 301 https://$server_name$request_uri;
}
Docker 部署
# Dockerfile
FROM python:3.11-slim
# 设置环境变量
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
# 设置工作目录
WORKDIR /app
# 安装系统依赖
RUN apt-get update && apt-get install -y \
postgresql-client \
build-essential \
&& rm -rf /var/lib/apt/lists/*
# 安装 Python 依赖
COPY requirements/production.txt /app/requirements.txt
RUN pip install --no-cache-dir -r requirements.txt
# 复制项目文件
COPY . /app
# 收集静态文件
RUN python manage.py collectstatic --noinput
# 创建非 root 用户
RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app
USER appuser
# 暴露端口
EXPOSE 8000
# 启动命令
CMD ["gunicorn", "myproject.wsgi:application", "--bind", "0.0.0.0:8000"]
# docker-compose.yml
version: '3.8'
services:
db:
image: postgres:15
environment:
POSTGRES_DB: mydb
POSTGRES_USER: user
POSTGRES_PASSWORD: password
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
redis:
image: redis:7-alpine
ports:
- "6379:6379"
web:
build: .
command: gunicorn myproject.wsgi:application --bind 0.0.0.0:8000
volumes:
- ./:/app
- static_files:/app/staticfiles
- media_files:/app/media
ports:
- "8000:8000"
env_file:
- .env
depends_on:
- db
- redis
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
- static_files:/app/staticfiles
- media_files:/app/media
depends_on:
- web
volumes:
postgres_data:
static_files:
media_files:
最佳实践
安全建议
# settings.py 生产环境配置
# HTTPS
SECURE_SSL_REDIRECT = True
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
# Cookies
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = 'Lax'
CSRF_COOKIE_SECURE = True
CSRF_COOKIE_HTTPONLY = True
# HSTS
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
# 其他安全设置
SECURE_CONTENT_TYPE_NOSNIFF = True
SECURE_BROWSER_XSS_FILTER = True
X_FRAME_OPTIONS = 'DENY'
# 密码验证
AUTH_PASSWORD_VALIDATORS = [
{'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'},
{'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 'OPTIONS': {'min_length': 12}},
{'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'},
{'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'},
]
# 白名单媒体类型
FILE_UPLOAD_HANDLERS = [
'django.core.files.uploadhandler.MemoryFileUploadHandler',
'django.core.files.uploadhandler.TemporaryFileUploadHandler',
]
性能优化
# 1. 数据库优化
# 使用 select_related 减少 ForeignKey 查询
articles = Article.objects.select_related('author', 'category').all()
# 使用 prefetch_related 减少 ManyToMany 查询
articles = Article.objects.prefetch_related('tags').all()
# 只查询需要的字段
articles = Article.objects.only('title', 'created_at').all()
# 延迟加载不常用的字段
articles = Article.objects.defer('content').all()
# 2. 缓存配置
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
'LOCATION': 'redis://127.0.0.1:6379/1',
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
}
}
}
# 使用缓存
from django.views.decorators.cache import cache_page
@cache_page(60 * 15) # 缓存15分钟
def article_list(request):
articles = Article.objects.all()
return render(request, 'articles.html', {'articles': articles})
# 3. 数据库连接池
# 使用 django-db-geventpool 或 pgBouncer
# 4. 静态文件 CDN
STATIC_URL = 'https://cdn.example.com/static/'
# 5. 异步任务
# 使用 Celery
from celery import shared_task
@shared_task
def send_email(user_id):
user = User.objects.get(id=user_id)
# 发送邮件
pass
测试
# tests/test_models.py
from django.test import TestCase
from myapp.models import Article, Category
class ArticleModelTest(TestCase):
@classmethod
def setUpTestData(cls):
cls.category = Category.objects.create(name='测试分类')
cls.article = Article.objects.create(
title='测试文章',
content='测试内容',
category=cls.category
)
def test_article_creation(self):
self.assertTrue(isinstance(self.article, Article))
self.assertEqual(self.article.title, '测试文章')
def test_article_category_relation(self):
self.assertEqual(self.article.category, self.category)
# tests/test_views.py
from django.test import Client
from django.urls import reverse
from myapp.models import Article
class ArticleViewTest(TestCase):
def setUp(self):
self.client = Client()
self.article = Article.objects.create(
title='测试文章',
content='测试内容'
)
def test_article_list_view(self):
response = self.client.get(reverse('article-list'))
self.assertEqual(response.status_code, 200)
self.assertContains(response, self.article.title)
def test_article_detail_view(self):
response = self.client.get(
reverse('article-detail', kwargs={'pk': self.article.pk})
)
self.assertEqual(response.status_code, 200)
self.assertContains(response, self.article.title)
# tests/test_api.py
from rest_framework.test import APITestCase
from myapp.models import Article
class ArticleAPITest(APITestCase):
def setUp(self):
self.article = Article.objects.create(
title='测试文章',
content='测试内容'
)
def test_get_articles(self):
url = reverse('article-list')
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.data), 1)
总结
Django 是一个功能强大的全栈 Web 框架:
核心概念
- MTV 架构:Model-Template-View 清晰分离
- ORM 系统:强大的数据库抽象层
- 自动 Admin:基于模型自动生成管理后台
- 内置认证:完整的用户认证和权限系统
- 模板系统:灵活的模板引擎
- 表单处理:强大的表单验证和处理
关键特性
- batteries included:开箱即用的完整功能
- 安全性高:内置多种安全防护
- 可扩展性强:丰富的第三方应用
- 适合大型项目:企业级应用开发
- REST Framework:强大的 API 开发支持
最佳实践
- 使用 MTV 架构组织代码
- 利用 ORM 操作数据库
- 使用 Admin 后台快速开发
- 实现完善的权限控制
- 编写测试保证质量
- 生产环境使用 Gunicorn + Nginx
- 使用缓存优化性能
- 配置文件环境分离
Django 适合快速开发大型企业级应用,是 Python Web 开发的首选框架之一。