引路者👇:

引言:

第一章:Django 视图与路由:构建灵活的后端入口

1.1 URL 路由进阶:精准匹配与模块化拆分

1.1.1 路径转换器:捕获动态 URL 参数

1.1.2 正则表达式:复杂 URL 模式匹配

1.1.3 命名空间与模块化路由:解决多 App 冲突

1. 根路由配置命名空间(project/urls.py)

2. App 内路由定义(blog/urls.py)

3. 反向解析路由(视图 / 模板中)

1.2 视图函数优化:简化代码与增强功能

1.2.1 快捷函数:减少重复逻辑

1.2.2 视图装饰器:增强视图功能

1. 限制 HTTP 方法(require_http_methods系列)

2. 用户认证与权限控制(login_required/permission_required)

3. 页面缓存(cache_page)

第二章:Django ORM 高级操作:高效处理数据库数据

2.1 高级查询:精准筛选与数据统计

2.1.1 条件查询:复杂筛选逻辑

2.1.2 聚合查询:统计数据(Count/Sum/Avg)

2.2 批量操作:提升大数据处理效率

2.2.1 批量创建:bulk_create()

2.2.2 批量更新:update()

2.2.3 批量删除:delete()

2.3 关联查询优化:避免 N+1 查询问题

第三章:Vue3 与 Django 交互:表单提交与异步处理

3.1 Vue3 表单设计:双向绑定与数据验证

3.2 Django 接收表单数据:处理 JSON 与表单格式

3.2.1 接收 JSON 格式数据(推荐)

3.2.2 接收表单格式数据

3.2.3 Vue3 发送表单格式数据的配置

第四章:跨域问题解决:Django CORS 配置

4.1 三种 CORS 配置方法(推荐第一种)

4.1.1 方法 1:使用django-cors-headers(推荐,功能完善)

1. 安装依赖

2. 配置settings.py

3. 前端配合配置(Axios 携带凭证)

4.1.2 方法 2:手动编写自定义中间件(无需第三方依赖)

4.1.3 方法 3:在视图中单独添加 CORS 头(适用于个别接口)

4.2 生产环境 CORS 配置注意事项

第五章:Promise 与异步处理:Vue3 中的异步请求优化

5.1 Promise 基础:管理异步状态

5.1.1 创建 Promise 对象

5.1.2 消费 Promise:then/catch/finally

5.2 Promise 链式调用:避免回调嵌套

5.3 async/await:简化异步代码

5.4 Promise 静态方法:并行处理多个异步请求

第六章:综合案例:Django+Vue3 实现产品管理系统

6.1 后端 Django 实现

1. 模型定义(models.py)

2. 视图函数(views.py)

3. URL 配置(urls.py)

4. CORS 配置(settings.py)

6.2 前端 Vue3 实现

第七章:总结


引言:

        在前后端分离开发模式中,Django 凭借强大的后端能力负责数据处理与接口提供,Vue3 则以灵活的组件化开发构建用户交互界面。本文Fly从 Django 的视图路由优化、ORM 高级操作,到 Vue3 的表单交互与异步处理,再到跨域问题解决,完整覆盖全栈开发的核心环节,附带可直接复用的代码示例,助力你快速落地项目。

第一章:Django 视图与路由:构建灵活的后端入口

        视图与路由是 Django 接收前端请求、分发业务逻辑的核心。合理的路由设计与视图封装,能让后端接口更易维护、扩展性更强。本节将深入讲解路由的高级配置与视图的实战技巧。

1.1 URL 路由进阶:精准匹配与模块化拆分

1.1.1 路径转换器:捕获动态 URL 参数

        Django 内置路径转换器,可快速提取 URL 中的动态参数(如文章 ID、分类别名),无需手动解析字符串,大幅简化代码。

转换器 功能描述 示例代码
str 匹配除/外的任意字符(默认类型) path('user/<str:username>/', views.user_detail)
int 匹配非负整数(常用于 ID、页码等场景) path('article/<int:pk>/', views.article_detail)
slug 匹配字母、数字、_-组成的 URL 友好字符串 path('category/<slug:alias>/', views.category_articles)
uuid 匹配 UUID 格式字符串(唯一性强,适合安全场景) path('order/<uuid:order_id>/', views.order_detail)
path 匹配包含/的完整路径(如文件路径) path('file/<path:file_path>/', views.file_download)

实战示例:通过转换器实现文章详情与分类列表

# myapp/urls.py
from django.urls import path
from . import views

urlpatterns = [
    # 文章详情:URL格式 /article/1/ → 提取pk=1(文章ID)
    path('article/<int:pk>/', views.article_detail, name='article_detail'),
    # 分类文章:URL格式 /category/tech/ → 提取alias='tech'(分类别名)
    path('category/<slug:alias>/', views.category_articles, name='category_articles'),
    # 文章归档:URL格式 /archive/2025/08/ → 提取year=2025、month=08
    path('archive/<int:year>/<int:month>/', views.article_archive, name='article_archive'),
]

# myapp/views.py
from django.shortcuts import render, get_object_or_404
from .models import Article, Category

def article_detail(request, pk):
    """通过文章ID获取详情,不存在则返回404"""
    # get_object_or_404:查询失败时自动抛出404,替代try-except
    article = get_object_or_404(Article, pk=pk)
    return render(request, 'article_detail.html', {'article': article})

def category_articles(request, alias):
    """通过分类别名获取文章列表"""
    category = get_object_or_404(Category, alias=alias)
    articles = Article.objects.filter(category=category).order_by('-pub_date')
    return render(request, 'category_list.html', {'category': category, 'articles': articles})

1.1.2 正则表达式:复杂 URL 模式匹配

        对于更灵活的 URL 规则(如 “按年月归档”“多条件筛选”),可使用re_path结合正则表达式,实现精准匹配。

示例:匹配 “/archive/2025/08/” 格式的归档 URL

# myapp/urls.py
from django.urls import re_path
from . import views

urlpatterns = [
    # 正则表达式:匹配4位年份+2位月份(如2025/08)
    re_path(r'^archive/(?P<year>[0-9]{4})/(?P<month>[0-9]{2})/$', 
            views.article_archive, 
            name='article_archive'),
]

# myapp/views.py
from datetime import datetime
from django.shortcuts import render
from .models import Article

def article_archive(request, year, month):
    """获取指定年月的文章归档"""
    # 转换参数类型(URL提取的是字符串,需转为整数)
    year = int(year)
    month = int(month)
    
    # 筛选该年月的文章
    articles = Article.objects.filter(
        pub_date__year=year,
        pub_date__month=month
    ).order_by('-pub_date')
    
    return render(request, 'archive.html', {
        'articles': articles,
        'year': year,
        'month': month,
        'current_date': datetime.now().strftime('%Y-%m-%d')
    })

1.1.3 命名空间与模块化路由:解决多 App 冲突

        当项目包含多个 App 时,可能出现路由名称重复(如article_detail)。通过 “命名空间” 可区分不同 App 的路由,通过 “模块化拆分” 可让路由配置更清晰。

1. 根路由配置命名空间(project/urls.py)
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    # 为blog App配置命名空间'blog'
    path('blog/', include(('blog.urls', 'blog'), namespace='blog')),
    # 为shop App配置命名空间'shop'
    path('shop/', include(('shop.urls', 'shop'), namespace='shop')),
]
2. App 内路由定义(blog/urls.py)
# blog/urls.py
from django.urls import path
from . import views

# 必须指定app_name,与根路由的命名空间对应
app_name = 'blog'

urlpatterns = [
    path('article/<int:pk>/', views.article_detail, name='article_detail'),
    path('category/<slug:alias>/', views.category_articles, name='category_articles'),
]
3. 反向解析路由(视图 / 模板中)
# 视图中反向解析(views.py)
from django.urls import reverse

def test_view(request):
    # 解析blog App的article_detail路由:/blog/article/1/
    blog_url = reverse('blog:article_detail', args=[1])
    # 解析shop App的product_detail路由(假设存在)
    shop_url = reverse('shop:product_detail', args=[100])
    return render(request, 'test.html', {'blog_url': blog_url, 'shop_url': shop_url})
<!-- 模板中反向解析(test.html) -->
<a href="{% url 'blog:article_detail' 1 %}">博客文章1</a>
<a href="{% url 'shop:product_detail' 100 %}">商城商品100</a>

1.2 视图函数优化:简化代码与增强功能

1.2.1 快捷函数:减少重复逻辑

        Django 提供render()redirect()get_object_or_404()等快捷函数,可替代冗余的手动代码,提升开发效率。

快捷函数 功能描述
render(request, template, context) 渲染模板并返回HttpResponse,自动处理模板路径查找
redirect(to, *args, **kwargs) 重定向到指定 URL(支持路由名称、绝对路径、外部链接)
get_object_or_404(Model, **kwargs) 根据条件查询单个对象,不存在则返回 404(替代try-except Model.DoesNotExist
get_list_or_404(Model, **kwargs) 根据条件查询对象列表,为空则返回 404

示例:使用快捷函数简化视图

# myapp/views.py
from django.shortcuts import render, redirect, get_object_or_404, get_list_or_404
from .models import Article

def article_detail(request, pk):
    # 替代:try: article = Article.objects.get(pk=pk) except Article.DoesNotExist: raise Http404
    article = get_object_or_404(Article, pk=pk)
    return render(request, 'article_detail.html', {'article': article})

def published_articles(request):
    # 若没有已发布的文章,直接返回404
    articles = get_list_or_404(Article, is_published=True)
    return render(request, 'article_list.html', {'articles': articles})

def delete_article(request, pk):
    article = get_object_or_404(Article, pk=pk)
    article.delete()
    # 重定向到文章列表页(使用路由名称,避免硬编码URL)
    return redirect('article_list')

1.2.2 视图装饰器:增强视图功能

        装饰器可在不修改视图核心逻辑的前提下,为其添加额外功能(如限制 HTTP 方法、要求登录、缓存页面)。

1. 限制 HTTP 方法(require_http_methods系列)

        确保视图仅接收指定的 HTTP 方法(如GET/POST),非法方法自动返回 405(Method Not Allowed):

from django.views.decorators.http import require_http_methods, require_GET, require_POST

# 仅允许GET和POST方法
@require_http_methods(['GET', 'POST'])
def article_edit(request, pk):
    article = get_object_or_404(Article, pk=pk)
    if request.method == 'GET':
        # 显示编辑表单
        return render(request, 'article_edit.html', {'article': article})
    elif request.method == 'POST':
        # 处理表单提交
        title = request.POST.get('title')
        content = request.POST.get('content')
        article.title = title
        article.content = content
        article.save()
        return redirect('article_detail', pk=pk)

# 仅允许GET方法(简化版@require_http_methods(['GET']))
@require_GET
def article_list(request):
    articles = Article.objects.all().order_by('-pub_date')
    return render(request, 'article_list.html', {'articles': articles})

# 仅允许POST方法(简化版@require_http_methods(['POST']))
@require_POST
def article_delete(request, pk):
    article = get_object_or_404(Article, pk=pk)
    article.delete()
    return redirect('article_list')
2. 用户认证与权限控制(login_required/permission_required

        限制视图仅允许已登录用户或拥有特定权限的用户访问:

from django.contrib.auth.decorators import login_required, permission_required

# 仅允许已登录用户访问(未登录自动跳转到登录页)
@login_required
def user_profile(request):
    # request.user 为当前登录用户
    return render(request, 'profile.html', {'user': request.user})

# 仅允许拥有“修改文章”权限的用户访问
@permission_required('myapp.change_article')  # 权限格式:app_label.permission_name
def article_edit(request, pk):
    article = get_object_or_404(Article, pk=pk)
    # 处理编辑逻辑...
    return render(request, 'article_edit.html', {'article': article})
3. 页面缓存(cache_page

        对频繁访问且变化少的页面(如首页、热门文章)进行缓存,减少数据库查询,提升性能:

from django.views.decorators.cache import cache_page

# 缓存15分钟(单位:秒)
@cache_page(60 * 15)
def home_page(request):
    # 复杂查询(如热门文章、分类统计)
    hot_articles = Article.objects.order_by('-views')[:5]
    category_count = Category.objects.annotate(article_count=Count('article'))
    return render(request, 'home.html', {
        'hot_articles': hot_articles,
        'category_count': category_count
    })

第二章:Django ORM 高级操作:高效处理数据库数据

        ORM(对象关系映射)是 Django 的核心特性,通过 Python 代码即可操作数据库,无需编写 SQL。本节将讲解 ORM 的高级查询、批量操作与关联查询优化,助力处理复杂数据场景。

2.1 高级查询:精准筛选与数据统计

2.1.1 条件查询:复杂筛选逻辑

        除基础的filter()/exclude(),Django ORM 支持通过 “双下划线” 语法实现范围、模糊匹配、日期筛选等复杂条件。

条件类型 ORM 语法示例 对应 SQL 逻辑
大于 / 小于 filter(price__gt=100)/filter(price__lt=50) WHERE price > 100/WHERE price < 50
大于等于 / 小于等于 filter(pub_date__gte='2025-01-01') WHERE pub_date >= '2025-01-01'
模糊匹配 filter(title__contains='Python') WHERE title LIKE '%Python%'
开头 / 结尾匹配 filter(title__startswith='Django')/filter(title__endswith='教程') WHERE title LIKE 'Django%'/WHERE title LIKE '%教程'
范围查询 filter(id__in=[1,2,3])/filter(price__range=(50, 100)) WHERE id IN (1,2,3)/WHERE price BETWEEN 50 AND 100
日期筛选 filter(pub_date__year=2025)/filter(pub_date__week_day=1) WHERE YEAR(pub_date) = 2025/WHERE WEEKDAY(pub_date) = 0(周日)

示例:多条件组合查询

from django.db.models import Q
from .models import Article

# 1. 基础多条件(AND逻辑):2025年发布且标题含“Python”的已发布文章
articles1 = Article.objects.filter(
    pub_date__year=2025,
    title__contains='Python',
    is_published=True
)

# 2. 复杂逻辑(OR+AND):使用Q对象
# 查询“标题含Python”或“阅读量>1000”,且发布时间在2025年的文章
articles2 = Article.objects.filter(
    Q(title__contains='Python') | Q(views__gt=1000),  # OR逻辑
    pub_date__year=2025  # AND逻辑(与Q对象组合)
)

# 3. 排除条件:非“技术”分类且阅读量>=500的文章
articles3 = Article.objects.filter(
    views__gte=500
).exclude(
    category__name='技术'
)

2.1.2 聚合查询:统计数据(Count/Sum/Avg)

        通过aggregate()结合聚合函数,实现数据统计(如总数、求和、平均值),返回字典格式结果;通过annotate()实现分组聚合(如按分类统计文章数)。

示例:聚合函数使用

from django.db.models import Count, Sum, Avg, Max, Min
from .models import Article, Order

# 1. 基础聚合:统计文章总数、平均阅读量、最高阅读量
article_stats = Article.objects.aggregate(
    total=Count('id'),
    avg_views=Avg('views'),
    max_views=Max('views')
)
print(article_stats)  
# 输出:{'total': 100, 'avg_views': 250.5, 'max_views': 1000}

# 2. 分组聚合:按分类统计文章数量(并按数量降序)
category_article_count = Category.objects.annotate(
    count=Count('article')  # 按分类分组,统计每组的文章数
).order_by('-count')
print(category_article_count)  
# 输出:[<Category: 技术 (50)>, <Category: 生活 (30)>, ...]

# 3. 关联聚合:统计每个用户的订单总金额
user_order_stats = User.objects.annotate(
    total_amount=Sum('order__amount')  # 关联Order模型的amount字段求和
).filter(total_amount__gt=1000)  # 筛选总金额>1000的用户

2.2 批量操作:提升大数据处理效率

        对于大量数据(如批量导入、批量更新),使用 ORM 的批量操作方法,可减少数据库交互次数,大幅提升性能。

2.2.1 批量创建:bulk_create()

        一次性创建多个对象,仅需 1 次数据库交互(替代循环调用create()):

# 批量创建100篇测试文章
articles = [
    Article(
        title=f"测试文章{i}",
        content=f"文章{i}的内容...",
        category_id=1,
        is_published=True
    ) for i in range(1, 101)
]

# 批量插入(batch_size控制每次插入数量,避免单次请求过大)
Article.objects.bulk_create(articles, batch_size=50)

2.2.2 批量更新:update()

        一次性更新符合条件的所有对象,仅需 1 次数据库交互:

from django.db.models import F
from datetime import timezone

# 1. 批量更新状态:将“待审核”文章改为“已发布”
Article.objects.filter(status='pending').update(status='published')

# 2. 批量更新数值字段:所有文章阅读量+100(F()避免竞态条件)
Article.objects.all().update(views=F('views') + 100)

# 3. 批量更新时间字段:更新所有文章的最后修改时间
Article.objects.all().update(updated_at=timezone.now())

2.2.3 批量删除:delete()

        一次性删除符合条件的所有对象,注意谨慎使用(无回收站,删除后不可恢复):

from datetime import datetime, timedelta

# 1. 删除1年前的过期文章
one_year_ago = datetime.now() - timedelta(days=365)
Article.objects.filter(pub_date__lt=one_year_ago).delete()

# 2. 删除“测试”分类下的所有文章
Article.objects.filter(category__name='测试').delete()

2.3 关联查询优化:避免 N+1 查询问题

        “N+1 查询问题” 指:查询 N 个对象时,先执行 1 次查询获取主表数据,再执行 N 次查询获取每个对象的关联数据。Django 提供select_related()prefetch_related()解决该问题。

方法 适用场景 原理
select_related() 一对一(OneToOneField)、一对多(ForeignKey,正向查询) 执行 JOIN 查询,一次性获取主表 + 关联表数据
prefetch_related() 多对多(ManyToManyField)、一对多(反向查询) 执行 2 次独立查询,内存中关联数据

示例:优化关联查询

# 1. 优化一对多正向查询(Article → Category)
# 原写法(N+1问题):获取10篇文章,执行1次Article查询 + 10次Category查询
articles = Article.objects.all()[:10]
for article in articles:
    print(article.category.name)  # 每次循环执行1次Category查询

# 优化写法(1次查询):使用select_related() JOIN Category表
articles = Article.objects.select_related('category').all()[:10]
for article in articles:
    print(article.category.name)  # 无额外查询

# 2. 优化多对多查询(Article → Tag)
# 原写法(N+1问题):获取10篇文章,执行1次Article查询 + 10次Tag查询
articles = Article.objects.all()[:10]
for article in articles:
    print(article.tags.all())  # 每次循环执行1次Tag查询

# 优化写法(2次查询):使用prefetch_related()
articles = Article.objects.prefetch_related('tags').all()[:10]
for article in articles:
    print(article.tags.all())  # 无额外查询

# 3. 组合优化(多关联层级):获取文章的分类+标签+作者
articles = Article.objects.select_related('category', 'author').prefetch_related('tags').all()[:10]

第三章:Vue3 与 Django 交互:表单提交与异步处理

        前后端分离架构中,Vue3 通过 Axios 发送异步请求,与 Django 后端交互。本节将讲解 Vue3 的表单设计、异步请求处理,以及 Django 如何接收并处理前端数据。

3.1 Vue3 表单设计:双向绑定与数据验证

        Vue3 的v-model实现表单数据双向绑定,结合axios可快速将表单数据发送到 Django 后端。以下为 “文章发布” 表单示例:

<!-- src/components/ArticleForm.vue -->
<template>
  <div class="article-form">
    <h2>发布文章</h2>
    <form @submit.prevent="submitArticle">
      <!-- 文章标题 -->
      <div class="form-item">
        <label>标题:</label>
        <input 
          type="text" 
          v-model="form.title" 
          required 
          placeholder="请输入文章标题(不超过200字)"
          @blur="validateTitle"
        >
        <p class="error" v-if="titleError">{{ titleError }}</p>
      </div>
      
      <!-- 文章分类 -->
      <div class="form-item">
        <label>分类:</label>
        <select v-model="form.categoryId" required>
          <option value="">请选择分类</option>
          <option v-for="category in categories" :key="category.id" :value="category.id">
            {{ category.name }}
          </option>
        </select>
      </div>
      
      <!-- 文章内容 -->
      <div class="form-item">
        <label>内容:</label>
        <textarea 
          v-model="form.content" 
          required 
          rows="8"
          placeholder="请输入文章内容"
        ></textarea>
      </div>
      
      <!-- 提交按钮(加载状态) -->
      <button type="submit" :disabled="loading">
        {{ loading ? "发布中..." : "发布文章" }}
      </button>
    </form>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue';
import axios from 'axios';

// 表单数据
const form = ref({
  title: "",
  categoryId: "",
  content: ""
});

// 分类列表(从Django后端获取)
const categories = ref([]);
// 加载状态
const loading = ref(false);
// 标题验证错误
const titleError = ref("");

// 验证标题长度
const validateTitle = () => {
  if (form.value.title.length > 200) {
    titleError.value = "标题长度不能超过200字";
  } else {
    titleError.value = "";
  }
};

// 获取分类列表(组件挂载时执行)
const fetchCategories = async () => {
  try {
    const response = await axios.get("/api/categories/");
    categories.value = response.data.data;
  } catch (error) {
    console.error("获取分类失败:", error);
    alert("获取分类失败,请刷新页面重试");
  }
};

// 提交文章
const submitArticle = async () => {
  // 先验证表单
  validateTitle();
  if (titleError.value || !form.value.title || !form.value.categoryId || !form.value.content) {
    alert("请完善表单信息");
    return;
  }

  loading.value = true;
  try {
    // 发送POST请求到Django后端
    const response = await axios.post("/api/articles/", form.value);
    alert(`文章发布成功!文章ID:${response.data.data.id}`);
    // 重置表单
    form.value = { title: "", categoryId: "", content: "" };
  } catch (error) {
    console.error("发布失败:", error);
    alert(error.response?.data?.message || "发布失败,请稍后再试");
  } finally {
    loading.value = false;
  }
};

// 组件挂载时加载分类
onMounted(() => {
  fetchCategories();
});
</script>

<style scoped>
.article-form { max-width: 800px; margin: 20px auto; padding: 0 20px; }
.form-item { margin-bottom: 15px; }
label { display: block; margin-bottom: 5px; font-weight: bold; }
input, select, textarea { width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; }
.error { color: #dc3545; font-size: 0.9em; margin: 5px 0 0 0; }
button { padding: 10px 20px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; }
button:disabled { background: #6c757d; cursor: not-allowed; }
</style>

3.2 Django 接收表单数据:处理 JSON 与表单格式

        Django 可接收 Vue3 发送的两种数据格式:application/x-www-form-urlencoded(表单格式)和application/json(JSON 格式),以下为两种格式的处理示例。

3.2.1 接收 JSON 格式数据(推荐)

        Vue3 的 Axios 默认发送 JSON 格式数据,Django 需通过request.body解析:

# myapp/views.py
import json
from django.http import JsonResponse
from .models import Article, Category

def create_article(request):
    """接收Vue3发送的JSON数据,创建文章"""
    if request.method == "POST":
        try:
            # 解析JSON格式的请求体
            data = json.loads(request.body)
            title = data.get("title")
            category_id = data.get("categoryId")
            content = data.get("content")
            
            # 数据验证
            if not title or not category_id or not content:
                return JsonResponse({
                    "status": "error",
                    "message": "标题、分类、内容不能为空"
                }, status=400)
            
            # 验证分类是否存在
            category = get_object_or_404(Category, id=category_id)
            
            # 存储到数据库
            article = Article.objects.create(
                title=title,
                category=category,
                content=content,
                is_published=True
            )
            
            # 返回成功响应
            return JsonResponse({
                "status": "success",
                "message": "文章创建成功",
                "data": {
                    "id": article.id,
                    "title": article.title,
                    "pub_date": article.pub_date.strftime("%Y-%m-%d %H:%M:%S")
                }
            }, status=201)
        
        except json.JSONDecodeError:
            # 数据格式错误(非JSON)
            return JsonResponse({
                "status": "error",
                "message": "数据格式错误,请发送JSON格式"
            }, status=400)
    
    # 非POST请求
    return JsonResponse({
        "status": "error",
        "message": "仅支持POST请求"
    }, status=405)

3.2.2 接收表单格式数据

        若 Vue3 发送表单格式数据(需配置 Axios 的Content-Type),Django 可通过request.POST获取:

# myapp/views.py
from django.http import JsonResponse
from django.shortcuts import get_object_or_404
from .models import Article, Category

def create_article_form(request):
    """接收表单格式数据,创建文章"""
    if request.method == "POST":
        # 从request.POST获取表单数据
        title = request.POST.get("title")
        category_id = request.POST.get("categoryId")
        content = request.POST.get("content")
        
        # 数据验证(同JSON格式)
        if not title or not category_id or not content:
            return JsonResponse({
                "status": "error",
                "message": "标题、分类、内容不能为空"
            }, status=400)
        
        category = get_object_or_404(Category, id=category_id)
        article = Article.objects.create(
            title=title,
            category=category,
            content=content
        )
        
        return JsonResponse({
            "status": "success",
            "data": {"id": article.id}
        }, status=201)
    
    return JsonResponse({"status": "error", "message": "仅支持POST请求"}, status=405)

3.2.3 Vue3 发送表单格式数据的配置

        若需发送表单格式数据,需在 Axios 中配置Content-Type

// Vue3组件中配置Axios
const submitArticleForm = async () => {
  try {
    const formData = new FormData();
    formData.append("title", form.value.title);
    formData.append("categoryId", form.value.categoryId);
    formData.append("content", form.value.content);
    
    await axios.post("/api/articles/form/", formData, {
      headers: {
        "Content-Type": "multipart/form-data"  // 表单格式(支持文件上传)
      }
    });
    alert("发布成功");
  } catch (error) {
    console.error(error);
  }
};

第四章:跨域问题解决:Django CORS 配置

        前后端分离架构中,Vue3(如运行在http://localhost:8080)与 Django(如运行在http://localhost:8000)因域名 / 端口不同,会触发浏览器的 “同源策略”,导致请求被拦截。需通过 CORS(跨域资源共享)配置允许跨域访问。

4.1 三种 CORS 配置方法(推荐第一种)

4.1.1 方法 1:使用django-cors-headers(推荐,功能完善)

  django-cors-headers是 Django 官方推荐的跨域解决方案,支持细粒度控制允许的域名、方法、请求头。

1. 安装依赖
pip install django-cors-headers
2. 配置settings.py

        需在INSTALLED_APPS中注册应用,并在MIDDLEWARE中添加中间件(顺序必须靠前,建议放在SecurityMiddleware之前):

# settings.py
INSTALLED_APPS = [
    # Django内置应用...
    'corsheaders',  # 注册cors-headers应用
    'myapp',  # 自定义应用
]

# 中间件配置(corsheaders.middleware.CorsMiddleware必须靠前)
MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware',  # 跨域中间件
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    # 其他中间件...
]

# ------------------- CORS核心配置 -------------------
# 开发环境:允许所有域名(方便调试,生产环境禁用)
# CORS_ALLOW_ALL_ORIGINS = True

# 生产环境:指定允许的域名(推荐,更安全)
CORS_ALLOWED_ORIGINS = [
    "http://localhost:8080",  # Vue3开发环境默认端口
    "http://127.0.0.1:8080",
    "https://yourdomain.com",  # 生产环境域名
]

# 允许的HTTP方法(默认支持GET/POST,可扩展)
CORS_ALLOW_METHODS = [
    "DELETE", "GET", "OPTIONS", "PATCH", "POST", "PUT",
]

# 允许的请求头(如Authorization、Content-Type)
CORS_ALLOW_HEADERS = [
    "accept", "authorization", "content-type", "user-agent",
    "x-csrftoken", "x-requested-with",
]

# 是否允许携带凭证(如Cookies、Token,需前后端同时配置)
CORS_ALLOW_CREDENTIALS = True

# 预检请求(OPTIONS)的缓存时间(秒),减少重复请求
CORS_PREFLIGHT_MAX_AGE = 86400  # 24小时
3. 前端配合配置(Axios 携带凭证)

        若CORS_ALLOW_CREDENTIALS = True,Vue3 的 Axios 需开启withCredentials

// src/utils/axios.js
import axios from "axios";

const service = axios.create({
  baseURL: "http://127.0.0.1:8000/api",
  timeout: 5000,
  withCredentials: true  // 允许携带凭证
});

export default service;

4.1.2 方法 2:手动编写自定义中间件(无需第三方依赖)

        若不想安装额外包,可自定义中间件,手动添加 CORS 响应头:

# myapp/middleware.py
class CustomCorsMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        # 处理请求
        response = self.get_response(request)
        
        # 添加CORS响应头(允许Vue3开发环境跨域)
        response["Access-Control-Allow-Origin"] = "http://localhost:8080"
        response["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE, OPTIONS"
        response["Access-Control-Allow-Headers"] = "Content-Type, Authorization"
        response["Access-Control-Allow-Credentials"] = "true"
        
        return response

        在settings.py中注册中间件:

# settings.py
MIDDLEWARE = [
    'myapp.middleware.CustomCorsMiddleware',  # 自定义跨域中间件
    # 其他中间件...
]

4.1.3 方法 3:在视图中单独添加 CORS 头(适用于个别接口)

        若仅需个别接口支持跨域,可在视图函数中手动添加响应头:

# myapp/views.py
from django.http import JsonResponse

def cross_domain_api(request):
    """仅该接口支持跨域"""
    data = {"message": "支持跨域的接口数据"}
    response = JsonResponse(data)
    
    # 添加CORS头
    response["Access-Control-Allow-Origin"] = "http://localhost:8080"
    response["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS"
    response["Access-Control-Allow-Headers"] = "Content-Type, Authorization"
    
    return response

4.2 生产环境 CORS 配置注意事项

  1. 禁止使用CORS_ALLOW_ALL_ORIGINS = True:生产环境必须明确指定允许的域名,避免恶意网站跨域访问;
  2. 限制允许的方法和头:仅开放业务必需的 HTTP 方法(如GET/POST/PUT)和请求头,减少攻击面;
  3. 凭证携带需谨慎:若CORS_ALLOW_CREDENTIALS = TrueCORS_ALLOWED_ORIGINS不能使用通配符*,必须指定具体域名;
  4. 处理预检请求(OPTIONS):复杂请求(如带自定义头、PUT/DELETE方法)会先发送OPTIONS请求,django-cors-headers会自动处理,自定义中间件需手动支持。

第五章:Promise 与异步处理:Vue3 中的异步请求优化

        Vue3 中处理异步请求(如 Axios 调用)时,Promise是核心工具,可避免 “回调地狱”;async/await则是Promise的语法糖,让异步代码更易读。本节将讲解Promise的核心用法与async/await的实战应用。

5.1 Promise 基础:管理异步状态

  Promise用于管理异步操作的状态(pendingfulfilled/rejected),通过then()/catch()/finally()处理结果。

5.1.1 创建 Promise 对象

        模拟 “用户登录” 异步操作,成功返回用户信息,失败返回错误原因:

// 模拟登录接口(异步操作)
const login = (username, password) => {
  return new Promise((resolve, reject) => {
    // 模拟后端接口延迟(1秒)
    setTimeout(() => {
      if (username === "admin" && password === "123456") {
        // 成功:调用resolve返回结果
        resolve({
          userId: 1,
          username: "admin",
          role: "superuser"
        });
      } else {
        // 失败:调用reject返回错误
        reject(new Error("用户名或密码错误"));
      }
    }, 1000);
  });
};

5.1.2 消费 Promise:then/catch/finally

// 调用登录接口
login("admin", "123456")
  .then((userInfo) => {
    console.log("登录成功:", userInfo);
    // 后续操作:如存储Token、跳转首页
    localStorage.setItem("token", "xxx");
    window.location.href = "/home";
  })
  .catch((error) => {
    console.error("登录失败:", error.message);
    // 显示错误提示
    alert(error.message);
  })
  .finally(() => {
    console.log("登录请求结束(无论成功/失败)");
    // 关闭登录按钮的加载状态
    document.getElementById("loginBtn").disabled = false;
  });

5.2 Promise 链式调用:避免回调嵌套

        每个then()都会返回一个新的Promise,支持连续调用,实现 “依次执行多个异步操作”:

// 步骤1:登录 → 步骤2:获取用户权限 → 步骤3:获取用户菜单
login("admin", "123456")
  .then((userInfo) => {
    // 步骤1成功:携带用户ID请求权限
    return axios.get(`/api/permissions?userId=${userInfo.userId}`);
  })
  .then((permissionRes) => {
    const permissions = permissionRes.data;
    // 步骤2成功:携带权限请求菜单
    return axios.get(`/api/menus?role=${permissions.role}`);
  })
  .then((menuRes) => {
    const menus = menuRes.data;
    console.log("用户菜单:", menus);
    // 渲染菜单到页面
  })
  .catch((error) => {
    // 任一环节失败,都会进入catch
    console.error("流程失败:", error);
  });

5.3 async/await:简化异步代码

   async/await是 ES8 推出的语法糖,基于Promise实现,将异步代码 “同步化” 书写,更易读、易调试:

// 定义async函数(必须在async函数中使用await)
const initUser = async () => {
  // 关闭按钮加载状态
  const loginBtn = document.getElementById("loginBtn");
  loginBtn.disabled = true;

  try {
    // 等待登录结果(暂停执行,直到Promise完成)
    const userInfo = await login("admin", "123456");
    console.log("登录成功:", userInfo);
    
    // 等待权限请求结果
    const permissionRes = await axios.get(`/api/permissions?userId=${userInfo.userId}`);
    const permissions = permissionRes.data;
    
    // 等待菜单请求结果
    const menuRes = await axios.get(`/api/menus?role=${permissions.role}`);
    const menus = menuRes.data;
    console.log("用户菜单:", menus);
    
    // 渲染菜单
    renderMenus(menus);
  } catch (error) {
    // 所有await的错误,都会被catch捕获
    console.error("初始化失败:", error);
    alert(error.message);
  } finally {
    // 无论成功/失败,都会执行
    loginBtn.disabled = false;
  }
};

// 调用async函数
initUser();

5.4 Promise 静态方法:并行处理多个异步请求

        Vue3 中常需同时请求多个接口(如 “用户信息 + 未读消息数”),Promise的静态方法可高效处理并行请求。

静态方法 功能描述 适用场景
Promise.all() 接收多个 Promise,全部成功才返回结果数组;任一失败则立即返回错误 依赖多个接口数据才能渲染页面
Promise.race() 接收多个 Promise,返回第一个完成的结果(无论成功 / 失败) 超时控制(如 “接口 5 秒未响应则提示超时”)
Promise.allSettled() 接收多个 Promise,所有操作结束后返回每个结果的状态(status+value/reason 需知道所有请求的结果(成功 / 失败)
Promise.any() 接收多个 Promise,返回第一个成功的结果;全部失败才返回错误 多源数据备份(如 “优先请求 CDN,失败则请求本地”)

示例:使用Promise.all()并行请求

// 并行请求“用户信息”和“未读消息数”
const fetchUserInfo = axios.get("/api/user/1");
const fetchUnreadCount = axios.get("/api/messages/unread");

Promise.all([fetchUserInfo, fetchUnreadCount])
  .then(([userRes, countRes]) => {
    const userInfo = userRes.data;
    const unreadCount = countRes.data;
    // 渲染用户信息和未读消息数
    renderUserInfo(userInfo);
    renderUnreadCount(unreadCount);
  })
  .catch((error) => {
    console.error("任一请求失败:", error);
    alert("数据加载失败,请刷新页面");
  });

第六章:综合案例:Django+Vue3 实现产品管理系统

        结合前文知识点,实现一个简单的 “产品管理系统”,包含 “产品列表展示”“产品添加”“产品删除” 功能,完整覆盖前后端交互、跨域配置、ORM 操作。

6.1 后端 Django 实现

1. 模型定义(models.py)

from django.db import models

class Product(models.Model):
    """产品模型"""
    name = models.CharField(max_length=200, verbose_name="产品名称")
    price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="产品价格")
    description = models.TextField(blank=True, verbose_name="产品描述")
    created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
    is_active = models.BooleanField(default=True, verbose_name="是否激活")

    class Meta:
        verbose_name = "产品"
        verbose_name_plural = "产品"
        ordering = ["-created_at"]

2. 视图函数(views.py)

import json
from django.http import JsonResponse
from django.shortcuts import get_object_or_404
from .models import Product

# 1. 获取产品列表
def get_products(request):
    if request.method == "GET":
        products = Product.objects.filter(is_active=True).values("id", "name", "price", "created_at")
        products_list = list(products)
        # 格式化日期
        for p in products_list:
            p["created_at"] = p["created_at"].strftime("%Y-%m-%d %H:%M:%S")
        return JsonResponse({"status": "success", "data": products_list})
    return JsonResponse({"status": "error", "message": "仅支持GET"}, status=405)

# 2. 添加产品
def add_product(request):
    if request.method == "POST":
        try:
            data = json.loads(request.body)
            name = data.get("name")
            price = data.get("price")
            description = data.get("description", "")
            
            if not name or not price:
                return JsonResponse({"status": "error", "message": "名称和价格不能为空"}, status=400)
            
            product = Product.objects.create(
                name=name,
                price=price,
                description=description
            )
            
            return JsonResponse({
                "status": "success",
                "data": {"id": product.id, "name": product.name}
            }, status=201)
        except json.JSONDecodeError:
            return JsonResponse({"status": "error", "message": "数据格式错误"}, status=400)
    return JsonResponse({"status": "error", "message": "仅支持POST"}, status=405)

# 3. 删除产品
def delete_product(request, pk):
    if request.method == "DELETE":
        product = get_object_or_404(Product, pk=pk)
        product.delete()
        return JsonResponse({"status": "success", "message": "产品已删除"})
    return JsonResponse({"status": "error", "message": "仅支持DELETE"}, status=405)

3. URL 配置(urls.py)

from django.urls import path
from . import views

urlpatterns = [
    path("products/", views.get_products, name="get_products"),
    path("products/add/", views.add_product, name="add_product"),
    path("products/delete/<int:pk>/", views.delete_product, name="delete_product"),
]

4. CORS 配置(settings.py)

INSTALLED_APPS = [
    # ...其他应用
    'corsheaders',
    'myapp',
]

MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware',
    # ...其他中间件
]

# 允许Vue3开发环境跨域
CORS_ALLOWED_ORIGINS = ["http://localhost:8080"]
CORS_ALLOW_METHODS = ["GET", "POST", "DELETE"]
CORS_ALLOW_HEADERS = ["Content-Type"]

6.2 前端 Vue3 实现

<template>
  <div class="product-manager">
    <h2>产品管理系统</h2>
    
    <!-- 添加产品表单 -->
    <div class="add-form">
      <h3>添加产品</h3>
      <form @submit.prevent="addProduct">
        <div class="form-item">
          <label>产品名称:</label>
          <input type="text" v-model="form.name" required placeholder="请输入名称">
        </div>
        <div class="form-item">
          <label>产品价格:</label>
          <input type="number" step="0.01" v-model="form.price" required placeholder="请输入价格">
        </div>
        <div class="form-item">
          <label>产品描述:</label>
          <textarea v-model="form.description" placeholder="请输入描述"></textarea>
        </div>
        <button type="submit" :disabled="loading">
          {{ loading ? "添加中..." : "添加产品" }}
        </button>
      </form>
    </div>
    
    <!-- 产品列表 -->
    <div class="product-list">
      <h3>产品列表</h3>
      <div v-if="loadingList">加载中...</div>
      <div v-else-if="errorList">{{ errorList }}</div>
      <table v-else>
        <thead>
          <tr>
            <th>ID</th>
            <th>名称</th>
            <th>价格</th>
            <th>创建时间</th>
            <th>操作</th>
          </tr>
        </thead>
        <tbody>
          <tr v-for="product in products" :key="product.id">
            <td>{{ product.id }}</td>
            <td>{{ product.name }}</td>
            <td>¥{{ product.price }}</td>
            <td>{{ product.created_at }}</td>
            <td>
              <button @click="deleteProduct(product.id)" :disabled="loadingDelete === product.id">
                {{ loadingDelete === product.id ? "删除中..." : "删除" }}
              </button>
            </td>
          </tr>
        </tbody>
      </table>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue';
import axios from 'axios';

// 表单数据
const form = ref({
  name: "",
  price: "",
  description: ""
});

// 列表数据
const products = ref([]);
const loadingList = ref(false);
const errorList = ref("");

// 加载状态
const loading = ref(false);
const loadingDelete = ref(null);

// 获取产品列表
const fetchProducts = async () => {
  loadingList.value = true;
  errorList.value = "";
  try {
    const res = await axios.get("/api/products/");
    products.value = res.data.data;
  } catch (err) {
    errorList.value = "获取产品列表失败";
    console.error(err);
  } finally {
    loadingList.value = false;
  }
};

// 添加产品
const addProduct = async () => {
  loading.value = true;
  try {
    await axios.post("/api/products/add/", form.value);
    alert("产品添加成功!");
    // 重置表单
    form.value = { name: "", price: "", description: "" };
    // 重新加载列表
    fetchProducts();
  } catch (err) {
    alert(err.response?.data?.message || "添加失败");
    console.error(err);
  } finally {
    loading.value = false;
  }
};

// 删除产品
const deleteProduct = async (id) => {
  if (!confirm("确定要删除该产品吗?")) return;
  loadingDelete.value = id;
  try {
    await axios.delete(`/api/products/delete/${id}/`);
    alert("产品删除成功!");
    // 重新加载列表
    fetchProducts();
  } catch (err) {
    alert(err.response?.data?.message || "删除失败");
    console.error(err);
  } finally {
    loadingDelete.value = null;
  }
};

// 组件挂载时加载列表
onMounted(() => {
  fetchProducts();
});
</script>

<style scoped>
.product-manager { max-width: 1200px; margin: 20px auto; padding: 0 20px; }
.add-form { margin-bottom: 30px; padding: 20px; border: 1px solid #eee; border-radius: 4px; }
.form-item { margin-bottom: 15px; }
label { display: block; margin-bottom: 5px; font-weight: bold; }
input, textarea { width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; }
button { padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer; }
.add-form button { background: #28a745; color: white; }
.product-list button { background: #dc3545; color: white; }
button:disabled { background: #6c757d; cursor: not-allowed; }
table { width: 100%; border-collapse: collapse; margin-top: 10px; }
th, td { padding: 12px; text-align: left; border-bottom: 1px solid #ddd; }
th { background: #f8f9fa; }
</style>

第七章:总结

        本文从 Django 的视图路由、ORM 高级操作,到 Vue3 的表单交互、异步处理,再到跨域问题解决,完整覆盖了全栈开发的核心环节。通过实战案例,展示了如何从 0 到 1 实现前后端交互功能,关键总结如下:

  1. 视图路由:路径转换器与正则表达式实现灵活 URL 匹配,命名空间解决多 App 路由冲突,装饰器增强视图功能;
  2. ORM 操作:高级查询、批量操作、关联查询优化是提升后端性能的关键,避免 N+1 查询问题;
  3. 前后端交互:Vue3 通过 Axios 发送 JSON / 表单数据,Django 通过request.body/request.POST接收,CORS 配置解决跨域问题;
  4. 异步处理Promise管理异步状态,async/await简化异步代码,静态方法高效处理并行请求。

如果实践操作中遇到其他问题,也可以在评论区留言,Fly帮你在线答疑!!!

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐