实战教程:基于Vue.js与Django REST Framework的任务管理SPA开发全流程

引言

在当今快速发展的Web开发领域,前后端分离架构已成为主流趋势。本文将带您从零开始构建一个功能完整的任务管理单页应用(SPA),结合Vue.js前端框架与Django REST Framework后端API服务。通过这个实战项目,您不仅能掌握现代Web开发的核心技术栈,还能学习到企业级应用开发的最佳实践。

一、项目概述与技术选型

1.1 项目功能需求

我们将开发一个具备以下核心功能的任务管理系统:

  • 用户认证:注册、登录、登出

  • 任务管理:创建、查看、更新、删除任务

  • 任务分类:按状态(待办、进行中、已完成)筛选

  • 搜索功能:按标题或内容搜索任务

  • 响应式设计:适配不同设备屏幕

1.2 技术栈选择

技术作用优势分析
Vue 3前端框架响应式、组合式API、良好生态
Vue Router前端路由管理SPA路由控制、导航守卫
Pinia状态管理轻量级、TypeScript支持
AxiosHTTP客户端Promise API、拦截器支持
Django后端框架ORM强大、Admin后台、安全性高
DRFREST API构建序列化、认证、权限、视图集
JWT认证机制无状态、跨域支持、安全性好

二、后端开发:Django REST Framework实现

2.1 项目初始化

# 创建Django项目
django-admin startproject taskmanager_backend
cd taskmanager_backend

# 创建核心应用
python manage.py startapp tasks
python manage.py startapp users

# 安装必要依赖
pip install djangorestframework django-cors-headers pyjwt

2.2 数据模型设计

tasks/models.py:

from django.db import models
from django.contrib.auth import get_user_model

User = get_user_model()

class Task(models.Model):
    STATUS_CHOICES = [
        ('TODO', '待办'),
        ('IN_PROGRESS', '进行中'),
        ('DONE', '已完成'),
    ]
    
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    title = models.CharField(max_length=200)
    description = models.TextField(blank=True)
    status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='TODO')
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    due_date = models.DateTimeField(null=True, blank=True)

    def __str__(self):
        return self.title

2.3 序列化器实现

tasks/serializers.py:

from rest_framework import serializers
from .models import Task
from users.serializers import UserSerializer

class TaskSerializer(serializers.ModelSerializer):
    user = UserSerializer(read_only=True)
    
    class Meta:
        model = Task
        fields = ['id', 'user', 'title', 'description', 'status', 
                 'created_at', 'updated_at', 'due_date']
        read_only_fields = ['id', 'user', 'created_at', 'updated_at']

2.4 视图集与路由配置

tasks/views.py:

from rest_framework import viewsets, permissions
from .models import Task
from .serializers import TaskSerializer
from .permissions import IsOwnerOrReadOnly

class TaskViewSet(viewsets.ModelViewSet):
    serializer_class = TaskSerializer
    permission_classes = [permissions.IsAuthenticated, IsOwnerOrReadOnly]
    
    def get_queryset(self):
        # 只返回当前用户的任务
        return Task.objects.filter(user=self.request.user)
    
    def perform_create(self, serializer):
        # 创建时自动关联当前用户
        serializer.save(user=self.request.user)

tasks/permissions.py:

from rest_framework import permissions

class IsOwnerOrReadOnly(permissions.BasePermission):
    """
    自定义权限:只允许任务的所有者编辑
    """
    def has_object_permission(self, request, view, obj):
        # 安全方法(GET, HEAD, OPTIONS)允许所有请求
        if request.method in permissions.SAFE_METHODS:
            return True
        
        # 写入权限仅限任务所有者
        return obj.user == request.user

2.5 JWT认证实现

users/views.py:

from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework_simplejwt.tokens import RefreshToken
from django.contrib.auth import authenticate
from .serializers import UserSerializer

class RegisterView(APIView):
    def post(self, request):
        serializer = UserSerializer(data=request.data)
        if serializer.is_valid():
            user = serializer.save()
            refresh = RefreshToken.for_user(user)
            return Response({
                'user': serializer.data,
                'refresh': str(refresh),
                'access': str(refresh.access_token),
            }, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

class LoginView(APIView):
    def post(self, request):
        username = request.data.get('username')
        password = request.data.get('password')
        user = authenticate(username=username, password=password)
        
        if user is None:
            return Response(
                {'error': 'Invalid credentials'},
                status=status.HTTP_401_UNAUTHORIZED
            )
            
        refresh = RefreshToken.for_user(user)
        return Response({
            'refresh': str(refresh),
            'access': str(refresh.access_token),
        })

三、前端开发:Vue.js实现

3.1 项目初始化

# 使用Vite创建Vue项目
npm create vite@latest taskmanager_frontend --template vue
cd taskmanager_frontend

# 安装必要依赖
npm install vue-router@4 pinia axios vue-axios
npm install @vueuse/core lodash-es
npm install --save-dev sass

3.2 项目结构设计

src/
├── api/                # API请求封装
├── assets/             # 静态资源
├── components/         # 公共组件
│   ├── TaskCard.vue
│   ├── TaskForm.vue
│   └── ...
├── composables/        # 组合式函数
├── router/             # 路由配置
├── stores/             # Pinia状态管理
├── styles/             # 全局样式
├── utils/              # 工具函数
├── views/              # 页面组件
│   ├── Auth/
│   ├── Dashboard/
│   └── ...
├── App.vue
└── main.js

3.3 Pinia状态管理

stores/authStore.js:

import { defineStore } from 'pinia'
import { ref } from 'vue'
import { login, register, logout } from '@/api/auth'
import router from '@/router'

export const useAuthStore = defineStore('auth', () => {
  const user = ref(null)
  const token = ref(localStorage.getItem('token'))
  const isAuthenticated = ref(false)

  const setAuth = (userData, authToken) => {
    user.value = userData
    token.value = authToken
    isAuthenticated.value = true
    localStorage.setItem('token', authToken)
  }

  const clearAuth = () => {
    user.value = null
    token.value = null
    isAuthenticated.value = false
    localStorage.removeItem('token')
  }

  const handleLogin = async (credentials) => {
    try {
      const response = await login(credentials)
      setAuth(response.user, response.access)
      router.push('/dashboard')
    } catch (error) {
      clearAuth()
      throw error
    }
  }

  const handleRegister = async (userData) => {
    try {
      const response = await register(userData)
      setAuth(response.user, response.access)
      router.push('/dashboard')
    } catch (error) {
      clearAuth()
      throw error
    }
  }

  const handleLogout = async () => {
    await logout()
    clearAuth()
    router.push('/login')
  }

  return {
    user,
    token,
    isAuthenticated,
    handleLogin,
    handleRegister,
    handleLogout
  }
})

3.4 API服务封装

api/tasks.js:

import axios from 'axios'
import { useAuthStore } from '@/stores/authStore'

const apiClient = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL,
  timeout: 10000
})

// 请求拦截器
apiClient.interceptors.request.use((config) => {
  const authStore = useAuthStore()
  if (authStore.token) {
    config.headers.Authorization = `Bearer ${authStore.token}`
  }
  return config
}, (error) => {
  return Promise.reject(error)
})

// 响应拦截器
apiClient.interceptors.response.use((response) => {
  return response
}, (error) => {
  if (error.response?.status === 401) {
    const authStore = useAuthStore()
    authStore.handleLogout()
  }
  return Promise.reject(error)
})

export default {
  getTasks(params = {}) {
    return apiClient.get('/tasks/', { params })
  },
  getTask(id) {
    return apiClient.get(`/tasks/${id}/`)
  },
  createTask(taskData) {
    return apiClient.post('/tasks/', taskData)
  },
  updateTask(id, taskData) {
    return apiClient.patch(`/tasks/${id}/`, taskData)
  },
  deleteTask(id) {
    return apiClient.delete(`/tasks/${id}/`)
  }
}

3.5 任务列表组件实现

components/TaskList.vue:

<script setup>
import { computed, ref } from 'vue'
import { useTaskStore } from '@/stores/taskStore'
import TaskCard from './TaskCard.vue'
import TaskForm from './TaskForm.vue'

const taskStore = useTaskStore()
const showForm = ref(false)
const editingTask = ref(null)

const tasks = computed(() => taskStore.tasks)
const filteredTasks = computed(() => {
  return tasks.value.filter(task => {
    // 根据状态筛选逻辑
    return true
  })
})

const handleEdit = (task) => {
  editingTask.value = task
  showForm.value = true
}

const handleSubmit = async (taskData) => {
  if (editingTask.value) {
    await taskStore.updateTask(editingTask.value.id, taskData)
  } else {
    await taskStore.createTask(taskData)
  }
  showForm.value = false
  editingTask.value = null
}
</script>

<template>
  <div class="task-list">
    <button @click="showForm = true" class="add-button">
      添加任务
    </button>
    
    <TaskForm 
      v-if="showForm"
      :initial-data="editingTask"
      @submit="handleSubmit"
      @cancel="showForm = false"
    />
    
    <div v-if="filteredTasks.length" class="tasks-grid">
      <TaskCard
        v-for="task in filteredTasks"
        :key="task.id"
        :task="task"
        @edit="handleEdit"
      />
    </div>
    
    <p v-else class="empty-message">
      暂无任务,点击上方按钮添加
    </p>
  </div>
</template>

<style scoped>
.task-list {
  max-width: 1200px;
  margin: 0 auto;
  padding: 20px;
}

.add-button {
  background-color: #4CAF50;
  color: white;
  padding: 10px 20px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  margin-bottom: 20px;
}

.tasks-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: 20px;
}

.empty-message {
  text-align: center;
  color: #666;
  font-size: 1.2rem;
}
</style>

四、前后端联调与部署

4.1 跨域问题解决

taskmanager_backend/settings.py:

INSTALLED_APPS = [
    ...
    'corsheaders',
]

MIDDLEWARE = [
    ...
    'corsheaders.middleware.CorsMiddleware',
    'django.middleware.common.CommonMiddleware',
]

# 允许所有来源(生产环境应配置具体域名)
CORS_ALLOW_ALL_ORIGINS = True

# 或指定允许的域名
CORS_ALLOWED_ORIGINS = [
    "http://localhost:5173",
    "https://your-production-domain.com"
]

4.2 环境变量配置

.env.development:

VITE_API_BASE_URL=http://localhost:8000/api

vue.config.js:

import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig(({ mode }) => {
  const env = loadEnv(mode, process.cwd())
  
  return {
    plugins: [vue()],
    server: {
      proxy: {
        '/api': {
          target: env.VITE_API_BASE_URL,
          changeOrigin: true,
          rewrite: path => path.replace(/^\/api/, '')
        }
      }
    }
  }
})

4.3 生产环境部署

后端部署(Nginx + Gunicorn):

安装Gunicorn:

pip install gunicorn

创建Gunicorn服务:

gunicorn --workers 3 --bind unix:taskmanager.sock taskmanager_backend.wsgi:application

Nginx配置:

server {
    listen 80;
    server_name api.yourdomain.com;
    
    location / {
        include proxy_params;
        proxy_pass http://unix:/path/to/taskmanager.sock;
    }
    
    location /static/ {
        alias /path/to/your/project/staticfiles/;
    }
}

前端部署:

构建生产版本:

npm run build

Nginx配置:

server {
    listen 80;
    server_name yourdomain.com;
    
    root /path/to/taskmanager_frontend/dist;
    index index.html;
    
    location / {
        try_files $uri $uri/ /index.html;
    }
    
    location /api {
        proxy_pass http://api.yourdomain.com;
    }
}

五、项目优化与扩展

5.1 性能优化

前端优化:

  • 组件懒加载

const Dashboard = () => import('@/views/Dashboard.vue')
  • 路由懒加载

{
  path: '/dashboard',
  component: () => import('@/views/Dashboard.vue')
}
  • 使用v-memo优化大型列表渲染

后端优化:

  • 数据库查询优化(select_related/prefetch_related)

  • 分页支持

  • 缓存常用数据

5.2 功能扩展

  1. 实时更新:

    • 使用WebSocket实现任务实时同步

    • 集成Django Channels

  2. 文件上传:

    • 实现任务附件功能

    • 使用Django的FileField和Vue的文件上传组件

  3. 数据可视化:

    • 使用ECharts或Chart.js展示任务统计图表

    • 实现任务完成情况的时间线视图

5.3 测试策略

  1. 前端测试:

    • 单元测试:Vitest + Vue Test Utils

    • E2E测试:Cypress

  2. 后端测试:

    • 单元测试:Django TestCase

    • API测试:DRF APITestCase

六、总结与展望

通过本项目的实践,我们完成了:

  1. 基于Django REST Framework构建了功能完善的RESTful API后端

  2. 使用Vue 3组合式API开发了响应式的前端SPA应用

  3. 实现了JWT认证的安全机制

  4. 掌握了前后端分离架构的开发流程

  5. 学习了项目部署的基本方法

项目亮点:

  • 采用现代化技术栈,符合当前行业趋势

  • 完善的认证与权限控制

  • 响应式设计,适配多种设备

  • 清晰的代码结构与模块化设计

未来改进方向:

  1. 增加团队协作功能,支持多人任务分配

  2. 实现任务提醒和通知系统

  3. 集成第三方登录(Google、GitHub等)

  4. 开发移动端应用(React Native或Flutter)

希望这篇实战教程能帮助您掌握Vue+Django REST Framework全栈开发技能!如果您在实践过程中遇到任何问题,欢迎在评论区留言讨论。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值