简体中文 繁體中文 English 日本語 Deutsch 한국 사람 بالعربية TÜRKÇE português คนไทย Français

站内搜索

搜索

活动公告

11-02 12:46
10-23 09:32
通知:本站资源由网友上传分享,如有违规等问题请到版务模块进行投诉,将及时处理!
10-23 09:31
10-23 09:28
通知:签到时间调整为每日4:00(东八区)
10-23 09:26

Vue3与Django交互实战从零开始构建前后端分离应用掌握API设计数据通信状态管理及安全认证的完整流程

3万

主题

349

科技点

3万

积分

大区版主

木柜子打湿

积分
31898

三倍冰淇淋无人之境【一阶】财Doro小樱(小丑装)立华奏以外的星空【二阶】⑨的冰沙

发表于 2025-9-17 22:10:06 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

x
引言

前后端分离是现代Web应用开发的主流架构模式,它将用户界面(UI)与业务逻辑、数据处理分离开来,使得前后端可以独立开发、测试和部署。Vue3作为当前流行的前端框架,以其响应式数据绑定、组件化开发和轻量级特性受到开发者青睐;而Django作为Python生态中成熟的Web框架,以其” batteries-included “的理念和强大的后台管理能力著称。本文将详细介绍如何从零开始使用Vue3和Django构建一个完整的前后端分离应用,涵盖API设计、数据通信、状态管理及安全认证等关键环节。

环境搭建

前端环境准备

首先,确保你的系统已安装Node.js(推荐版本14.x或更高)。然后,使用npm安装Vue CLI:
  1. npm install -g @vue/cli
复制代码

检查Vue CLI版本:
  1. vue --version
复制代码

后端环境准备

确保你的系统已安装Python(推荐版本3.8或更高)。然后,使用pip安装Django和Django REST framework:
  1. pip install django djangorestframework djangorestframework-simplejwt django-cors-headers
复制代码

Django后端开发

项目创建与配置

创建Django项目:
  1. django-admin startproject backend
  2. cd backend
复制代码

创建一个应用:
  1. python manage.py startapp api
复制代码

在backend/settings.py中,添加新创建的应用和必要的库到INSTALLED_APPS:
  1. INSTALLED_APPS = [
  2.     'django.contrib.admin',
  3.     'django.contrib.auth',
  4.     'django.contrib.contenttypes',
  5.     'django.contrib.sessions',
  6.     'django.contrib.messages',
  7.     'django.contrib.staticfiles',
  8.     'rest_framework',
  9.     'rest_framework_simplejwt',
  10.     'corsheaders',
  11.     'api',
  12. ]
复制代码

添加中间件以处理CORS:
  1. MIDDLEWARE = [
  2.     'corsheaders.middleware.CorsMiddleware',
  3.     'django.middleware.security.SecurityMiddleware',
  4.     'django.contrib.sessions.middleware.SessionMiddleware',
  5.     'django.middleware.common.CommonMiddleware',
  6.     'django.middleware.csrf.CsrfViewMiddleware',
  7.     'django.contrib.auth.middleware.AuthenticationMiddleware',
  8.     'django.contrib.messages.middleware.MessageMiddleware',
  9.     'django.middleware.clickjacking.XFrameOptionsMiddleware',
  10. ]
复制代码

配置CORS设置:
  1. CORS_ALLOWED_ORIGINS = [
  2.     "http://localhost:8080",
  3.     "http://127.0.0.1:8080",
  4. ]
  5. CORS_ALLOW_CREDENTIALS = True
复制代码

配置REST framework和JWT认证:
  1. REST_FRAMEWORK = {
  2.     'DEFAULT_AUTHENTICATION_CLASSES': (
  3.         'rest_framework_simplejwt.authentication.JWTAuthentication',
  4.     ),
  5.     'DEFAULT_PERMISSION_CLASSES': [
  6.         'rest_framework.permissions.IsAuthenticated',
  7.     ],
  8. }
  9. from datetime import timedelta
  10. SIMPLE_JWT = {
  11.     'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60),
  12.     'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
  13.     'ROTATE_REFRESH_TOKENS': False,
  14.     'BLACKLIST_AFTER_ROTATION': True,
  15.     'ALGORITHM': 'HS256',
  16.     'SIGNING_KEY': 'your-signing-key',
  17.     'VERIFYING_KEY': None,
  18.     'AUTH_HEADER_TYPES': ('Bearer',),
  19.     'USER_ID_FIELD': 'id',
  20.     'USER_ID_CLAIM': 'user_id',
  21.     'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',),
  22.     'TOKEN_TYPE_CLAIM': 'token_type',
  23. }
复制代码

模型设计

在api/models.py中定义数据模型。假设我们正在构建一个简单的任务管理系统:
  1. from django.db import models
  2. from django.contrib.auth.models import User
  3. class Task(models.Model):
  4.     STATUS_CHOICES = (
  5.         ('pending', 'Pending'),
  6.         ('in_progress', 'In Progress'),
  7.         ('completed', 'Completed'),
  8.     )
  9.    
  10.     PRIORITY_CHOICES = (
  11.         ('low', 'Low'),
  12.         ('medium', 'Medium'),
  13.         ('high', 'High'),
  14.     )
  15.    
  16.     title = models.CharField(max_length=200)
  17.     description = models.TextField(blank=True)
  18.     status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending')
  19.     priority = models.CharField(max_length=10, choices=PRIORITY_CHOICES, default='medium')
  20.     created_at = models.DateTimeField(auto_now_add=True)
  21.     updated_at = models.DateTimeField(auto_now=True)
  22.     due_date = models.DateField(null=True, blank=True)
  23.     owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name='tasks')
  24.    
  25.     def __str__(self):
  26.         return self.title
复制代码

应用数据库迁移:
  1. python manage.py makemigrations
  2. python manage.py migrate
复制代码

序列化器

在api/serializers.py中创建序列化器:
  1. from rest_framework import serializers
  2. from django.contrib.auth.models import User
  3. from .models import Task
  4. class UserSerializer(serializers.ModelSerializer):
  5.     class Meta:
  6.         model = User
  7.         fields = ('id', 'username', 'email')
  8. class TaskSerializer(serializers.ModelSerializer):
  9.     owner = UserSerializer(read_only=True)
  10.    
  11.     class Meta:
  12.         model = Task
  13.         fields = '__all__'
复制代码

视图和路由

在api/views.py中创建视图:
  1. from rest_framework import viewsets, permissions, status
  2. from rest_framework.decorators import action
  3. from rest_framework.response import Response
  4. from rest_framework_simplejwt.tokens import RefreshToken
  5. from django.contrib.auth import authenticate
  6. from .models import Task
  7. from .serializers import TaskSerializer, UserSerializer
  8. class TaskViewSet(viewsets.ModelViewSet):
  9.     queryset = Task.objects.all()
  10.     serializer_class = TaskSerializer
  11.     permission_classes = [permissions.IsAuthenticated]
  12.    
  13.     def get_queryset(self):
  14.         return Task.objects.filter(owner=self.request.user)
  15.    
  16.     def perform_create(self, serializer):
  17.         serializer.save(owner=self.request.user)
  18. class AuthViewSet(viewsets.ViewSet):
  19.     permission_classes = [permissions.AllowAny]
  20.    
  21.     @action(detail=False, methods=['post'])
  22.     def register(self, request):
  23.         username = request.data.get('username')
  24.         password = request.data.get('password')
  25.         email = request.data.get('email', '')
  26.         
  27.         if not username or not password:
  28.             return Response({'error': 'Username and password are required'},
  29.                            status=status.HTTP_400_BAD_REQUEST)
  30.         
  31.         if User.objects.filter(username=username).exists():
  32.             return Response({'error': 'Username already exists'},
  33.                            status=status.HTTP_400_BAD_REQUEST)
  34.         
  35.         user = User.objects.create_user(username=username, password=password, email=email)
  36.         refresh = RefreshToken.for_user(user)
  37.         
  38.         return Response({
  39.             'user': UserSerializer(user).data,
  40.             'tokens': {
  41.                 'refresh': str(refresh),
  42.                 'access': str(refresh.access_token),
  43.             }
  44.         }, status=status.HTTP_201_CREATED)
  45.    
  46.     @action(detail=False, methods=['post'])
  47.     def login(self, request):
  48.         username = request.data.get('username')
  49.         password = request.data.get('password')
  50.         
  51.         user = authenticate(username=username, password=password)
  52.         
  53.         if user:
  54.             refresh = RefreshToken.for_user(user)
  55.             return Response({
  56.                 'user': UserSerializer(user).data,
  57.                 'tokens': {
  58.                     'refresh': str(refresh),
  59.                     'access': str(refresh.access_token),
  60.                 }
  61.             })
  62.         
  63.         return Response({'error': 'Invalid credentials'},
  64.                        status=status.HTTP_401_UNAUTHORIZED)
  65.    
  66.     @action(detail=False, methods=['post'])
  67.     def refresh(self, request):
  68.         refresh_token = request.data.get('refresh')
  69.         
  70.         if not refresh_token:
  71.             return Response({'error': 'Refresh token is required'},
  72.                            status=status.HTTP_400_BAD_REQUEST)
  73.         
  74.         try:
  75.             refresh = RefreshToken(refresh_token)
  76.             access_token = str(refresh.access_token)
  77.             return Response({
  78.                 'access': access_token,
  79.             })
  80.         except Exception as e:
  81.             return Response({'error': 'Invalid refresh token'},
  82.                            status=status.HTTP_401_UNAUTHORIZED)
复制代码

在backend/urls.py中配置路由:
  1. from django.contrib import admin
  2. from django.urls import path, include
  3. from rest_framework.routers import DefaultRouter
  4. from api.views import TaskViewSet, AuthViewSet
  5. router = DefaultRouter()
  6. router.register(r'tasks', TaskViewSet, basename='task')
  7. router.register(r'auth', AuthViewSet, basename='auth', basename_prefix='auth')
  8. urlpatterns = [
  9.     path('admin/', admin.site.urls),
  10.     path('api/', include(router.urls)),
  11. ]
复制代码

运行Django开发服务器:
  1. python manage.py runserver
复制代码

Vue3前端开发

项目创建与配置

创建Vue3项目:
  1. vue create frontend
复制代码

在创建过程中,选择Vue 3预设。进入项目目录:
  1. cd frontend
复制代码

安装必要的依赖:
  1. npm install axios pinia vue-router
复制代码

项目结构

创建以下目录结构:
  1. frontend/
  2. ├── public/
  3. ├── src/
  4. │   ├── assets/
  5. │   ├── components/
  6. │   ├── views/
  7. │   ├── stores/
  8. │   ├── router/
  9. │   ├── api/
  10. │   ├── App.vue
  11. │   └── main.js
复制代码

API请求封装

在src/api/index.js中封装API请求:
  1. import axios from 'axios';
  2. const apiClient = axios.create({
  3.   baseURL: 'http://localhost:8000/api',
  4.   headers: {
  5.     'Content-Type': 'application/json',
  6.   },
  7.   withCredentials: true,
  8. });
  9. // 请求拦截器
  10. apiClient.interceptors.request.use(
  11.   (config) => {
  12.     const token = localStorage.getItem('access_token');
  13.     if (token) {
  14.       config.headers.Authorization = `Bearer ${token}`;
  15.     }
  16.     return config;
  17.   },
  18.   (error) => {
  19.     return Promise.reject(error);
  20.   }
  21. );
  22. // 响应拦截器
  23. apiClient.interceptors.response.use(
  24.   (response) => response,
  25.   async (error) => {
  26.     const originalRequest = error.config;
  27.    
  28.     // 如果是401错误且不是刷新令牌的请求
  29.     if (error.response.status === 401 && !originalRequest._retry) {
  30.       originalRequest._retry = true;
  31.       
  32.       try {
  33.         const refreshToken = localStorage.getItem('refresh_token');
  34.         const response = await apiClient.post('/auth/refresh/', {
  35.           refresh: refreshToken,
  36.         });
  37.         
  38.         const { access } = response.data;
  39.         localStorage.setItem('access_token', access);
  40.         
  41.         // 更新原始请求的Authorization头
  42.         originalRequest.headers.Authorization = `Bearer ${access}`;
  43.         
  44.         // 重新发送原始请求
  45.         return apiClient(originalRequest);
  46.       } catch (refreshError) {
  47.         // 刷新令牌失败,清除本地存储并重定向到登录页
  48.         localStorage.removeItem('access_token');
  49.         localStorage.removeItem('refresh_token');
  50.         window.location.href = '/login';
  51.         return Promise.reject(refreshError);
  52.       }
  53.     }
  54.    
  55.     return Promise.reject(error);
  56.   }
  57. );
  58. export default apiClient;
复制代码

在src/api/auth.js中创建认证相关的API函数:
  1. import apiClient from './index';
  2. export const authAPI = {
  3.   register: (userData) => apiClient.post('/auth/register/', userData),
  4.   login: (credentials) => apiClient.post('/auth/login/', credentials),
  5.   refreshToken: () => apiClient.post('/auth/refresh/', {
  6.     refresh: localStorage.getItem('refresh_token'),
  7.   }),
  8. };
  9. export const taskAPI = {
  10.   getAll: () => apiClient.get('/tasks/'),
  11.   get: (id) => apiClient.get(`/tasks/${id}/`),
  12.   create: (taskData) => apiClient.post('/tasks/', taskData),
  13.   update: (id, taskData) => apiClient.put(`/tasks/${id}/`, taskData),
  14.   delete: (id) => apiClient.delete(`/tasks/${id}/`),
  15. };
复制代码

状态管理(Pinia)

在src/stores/auth.js中创建认证状态管理:
  1. import { defineStore } from 'pinia';
  2. import { authAPI } from '@/api/auth';
  3. export const useAuthStore = defineStore('auth', {
  4.   state: () => ({
  5.     user: null,
  6.     accessToken: localStorage.getItem('access_token') || null,
  7.     refreshToken: localStorage.getItem('refresh_token') || null,
  8.     isAuthenticated: !!localStorage.getItem('access_token'),
  9.   }),
  10.   
  11.   getters: {
  12.     getUser: (state) => state.user,
  13.     isLoggedIn: (state) => state.isAuthenticated,
  14.   },
  15.   
  16.   actions: {
  17.     setAuth(user, tokens) {
  18.       this.user = user;
  19.       this.accessToken = tokens.access;
  20.       this.refreshToken = tokens.refresh;
  21.       this.isAuthenticated = true;
  22.       
  23.       localStorage.setItem('access_token', tokens.access);
  24.       localStorage.setItem('refresh_token', tokens.refresh);
  25.     },
  26.    
  27.     clearAuth() {
  28.       this.user = null;
  29.       this.accessToken = null;
  30.       this.refreshToken = null;
  31.       this.isAuthenticated = false;
  32.       
  33.       localStorage.removeItem('access_token');
  34.       localStorage.removeItem('refresh_token');
  35.     },
  36.    
  37.     async register(userData) {
  38.       try {
  39.         const response = await authAPI.register(userData);
  40.         this.setAuth(response.data.user, response.data.tokens);
  41.         return response;
  42.       } catch (error) {
  43.         throw error;
  44.       }
  45.     },
  46.    
  47.     async login(credentials) {
  48.       try {
  49.         const response = await authAPI.login(credentials);
  50.         this.setAuth(response.data.user, response.data.tokens);
  51.         return response;
  52.       } catch (error) {
  53.         throw error;
  54.       }
  55.     },
  56.    
  57.     async logout() {
  58.       this.clearAuth();
  59.     },
  60.   },
  61. });
复制代码

在src/stores/task.js中创建任务状态管理:
  1. import { defineStore } from 'pinia';
  2. import { taskAPI } from '@/api/task';
  3. export const useTaskStore = defineStore('task', {
  4.   state: () => ({
  5.     tasks: [],
  6.     currentTask: null,
  7.     loading: false,
  8.     error: null,
  9.   }),
  10.   
  11.   getters: {
  12.     getAllTasks: (state) => state.tasks,
  13.     getTaskById: (state) => (id) => state.tasks.find(task => task.id === id),
  14.     isLoading: (state) => state.loading,
  15.     getError: (state) => state.error,
  16.   },
  17.   
  18.   actions: {
  19.     setTasks(tasks) {
  20.       this.tasks = tasks;
  21.     },
  22.    
  23.     addTask(task) {
  24.       this.tasks.push(task);
  25.     },
  26.    
  27.     updateTask(updatedTask) {
  28.       const index = this.tasks.findIndex(task => task.id === updatedTask.id);
  29.       if (index !== -1) {
  30.         this.tasks[index] = updatedTask;
  31.       }
  32.     },
  33.    
  34.     removeTask(taskId) {
  35.       this.tasks = this.tasks.filter(task => task.id !== taskId);
  36.     },
  37.    
  38.     setLoading(loading) {
  39.       this.loading = loading;
  40.     },
  41.    
  42.     setError(error) {
  43.       this.error = error;
  44.     },
  45.    
  46.     async fetchTasks() {
  47.       this.setLoading(true);
  48.       this.setError(null);
  49.       
  50.       try {
  51.         const response = await taskAPI.getAll();
  52.         this.setTasks(response.data.results);
  53.         return response;
  54.       } catch (error) {
  55.         this.setError(error.message || 'Failed to fetch tasks');
  56.         throw error;
  57.       } finally {
  58.         this.setLoading(false);
  59.       }
  60.     },
  61.    
  62.     async fetchTask(id) {
  63.       this.setLoading(true);
  64.       this.setError(null);
  65.       
  66.       try {
  67.         const response = await taskAPI.get(id);
  68.         this.currentTask = response.data;
  69.         return response;
  70.       } catch (error) {
  71.         this.setError(error.message || 'Failed to fetch task');
  72.         throw error;
  73.       } finally {
  74.         this.setLoading(false);
  75.       }
  76.     },
  77.    
  78.     async createTask(taskData) {
  79.       this.setLoading(true);
  80.       this.setError(null);
  81.       
  82.       try {
  83.         const response = await taskAPI.create(taskData);
  84.         this.addTask(response.data);
  85.         return response;
  86.       } catch (error) {
  87.         this.setError(error.message || 'Failed to create task');
  88.         throw error;
  89.       } finally {
  90.         this.setLoading(false);
  91.       }
  92.     },
  93.    
  94.     async updateTask(id, taskData) {
  95.       this.setLoading(true);
  96.       this.setError(null);
  97.       
  98.       try {
  99.         const response = await taskAPI.update(id, taskData);
  100.         this.updateTask(response.data);
  101.         return response;
  102.       } catch (error) {
  103.         this.setError(error.message || 'Failed to update task');
  104.         throw error;
  105.       } finally {
  106.         this.setLoading(false);
  107.       }
  108.     },
  109.    
  110.     async deleteTask(id) {
  111.       this.setLoading(true);
  112.       this.setError(null);
  113.       
  114.       try {
  115.         const response = await taskAPI.delete(id);
  116.         this.removeTask(id);
  117.         return response;
  118.       } catch (error) {
  119.         this.setError(error.message || 'Failed to delete task');
  120.         throw error;
  121.       } finally {
  122.         this.setLoading(false);
  123.       }
  124.     },
  125.   },
  126. });
复制代码

路由管理

在src/router/index.js中配置路由:
  1. import { createRouter, createWebHistory } from 'vue-router';
  2. import { useAuthStore } from '@/stores/auth';
  3. const routes = [
  4.   {
  5.     path: '/',
  6.     name: 'Home',
  7.     component: () => import('@/views/Home.vue'),
  8.     meta: { requiresAuth: true },
  9.   },
  10.   {
  11.     path: '/login',
  12.     name: 'Login',
  13.     component: () => import('@/views/Login.vue'),
  14.     meta: { guest: true },
  15.   },
  16.   {
  17.     path: '/register',
  18.     name: 'Register',
  19.     component: () => import('@/views/Register.vue'),
  20.     meta: { guest: true },
  21.   },
  22.   {
  23.     path: '/tasks',
  24.     name: 'Tasks',
  25.     component: () => import('@/views/Tasks.vue'),
  26.     meta: { requiresAuth: true },
  27.   },
  28.   {
  29.     path: '/tasks/:id',
  30.     name: 'TaskDetail',
  31.     component: () => import('@/views/TaskDetail.vue'),
  32.     meta: { requiresAuth: true },
  33.   },
  34.   {
  35.     path: '/tasks/create',
  36.     name: 'CreateTask',
  37.     component: () => import('@/views/CreateTask.vue'),
  38.     meta: { requiresAuth: true },
  39.   },
  40.   {
  41.     path: '/tasks/:id/edit',
  42.     name: 'EditTask',
  43.     component: () => import('@/views/EditTask.vue'),
  44.     meta: { requiresAuth: true },
  45.   },
  46.   {
  47.     path: '/:pathMatch(.*)*',
  48.     name: 'NotFound',
  49.     component: () => import('@/views/NotFound.vue'),
  50.   },
  51. ];
  52. const router = createRouter({
  53.   history: createWebHistory(),
  54.   routes,
  55. });
  56. // 导航守卫
  57. router.beforeEach((to, from, next) => {
  58.   const authStore = useAuthStore();
  59.   const requiresAuth = to.matched.some(record => record.meta.requiresAuth);
  60.   const isGuest = to.matched.some(record => record.meta.guest);
  61.   
  62.   if (requiresAuth && !authStore.isAuthenticated) {
  63.     next('/login');
  64.   } else if (isGuest && authStore.isAuthenticated) {
  65.     next('/');
  66.   } else {
  67.     next();
  68.   }
  69. });
  70. export default router;
复制代码

视图组件

在src/views/Login.vue中创建登录页面:
  1. <template>
  2.   <div class="login-container">
  3.     <div class="login-form">
  4.       <h1>Login</h1>
  5.       <form @submit.prevent="handleLogin">
  6.         <div class="form-group">
  7.           <label for="username">Username</label>
  8.           <input
  9.             type="text"
  10.             id="username"
  11.             v-model="credentials.username"
  12.             required
  13.           />
  14.         </div>
  15.         <div class="form-group">
  16.           <label for="password">Password</label>
  17.           <input
  18.             type="password"
  19.             id="password"
  20.             v-model="credentials.password"
  21.             required
  22.           />
  23.         </div>
  24.         <div v-if="error" class="error-message">{{ error }}</div>
  25.         <button type="submit" :disabled="loading">
  26.           {{ loading ? 'Logging in...' : 'Login' }}
  27.         </button>
  28.       </form>
  29.       <p class="register-link">
  30.         Don't have an account? <router-link to="/register">Register</router-link>
  31.       </p>
  32.     </div>
  33.   </div>
  34. </template>
  35. <script>
  36. import { ref } from 'vue';
  37. import { useRouter } from 'vue-router';
  38. import { useAuthStore } from '@/stores/auth';
  39. export default {
  40.   name: 'Login',
  41.   setup() {
  42.     const router = useRouter();
  43.     const authStore = useAuthStore();
  44.    
  45.     const credentials = ref({
  46.       username: '',
  47.       password: '',
  48.     });
  49.    
  50.     const loading = ref(false);
  51.     const error = ref(null);
  52.    
  53.     const handleLogin = async () => {
  54.       loading.value = true;
  55.       error.value = null;
  56.       
  57.       try {
  58.         await authStore.login(credentials.value);
  59.         router.push('/');
  60.       } catch (err) {
  61.         error.value = err.response?.data?.error || 'Login failed';
  62.       } finally {
  63.         loading.value = false;
  64.       }
  65.     };
  66.    
  67.     return {
  68.       credentials,
  69.       loading,
  70.       error,
  71.       handleLogin,
  72.     };
  73.   },
  74. };
  75. </script>
  76. <style scoped>
  77. .login-container {
  78.   display: flex;
  79.   justify-content: center;
  80.   align-items: center;
  81.   min-height: 100vh;
  82.   background-color: #f5f5f5;
  83. }
  84. .login-form {
  85.   width: 100%;
  86.   max-width: 400px;
  87.   padding: 2rem;
  88.   background-color: white;
  89.   border-radius: 8px;
  90.   box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
  91. }
  92. h1 {
  93.   text-align: center;
  94.   margin-bottom: 1.5rem;
  95. }
  96. .form-group {
  97.   margin-bottom: 1rem;
  98. }
  99. label {
  100.   display: block;
  101.   margin-bottom: 0.5rem;
  102.   font-weight: bold;
  103. }
  104. input {
  105.   width: 100%;
  106.   padding: 0.75rem;
  107.   border: 1px solid #ddd;
  108.   border-radius: 4px;
  109.   font-size: 1rem;
  110. }
  111. button {
  112.   width: 100%;
  113.   padding: 0.75rem;
  114.   background-color: #4caf50;
  115.   color: white;
  116.   border: none;
  117.   border-radius: 4px;
  118.   font-size: 1rem;
  119.   cursor: pointer;
  120.   transition: background-color 0.3s;
  121. }
  122. button:hover {
  123.   background-color: #45a049;
  124. }
  125. button:disabled {
  126.   background-color: #cccccc;
  127.   cursor: not-allowed;
  128. }
  129. .error-message {
  130.   color: #f44336;
  131.   margin-bottom: 1rem;
  132.   text-align: center;
  133. }
  134. .register-link {
  135.   text-align: center;
  136.   margin-top: 1rem;
  137. }
  138. a {
  139.   color: #2196f3;
  140.   text-decoration: none;
  141. }
  142. a:hover {
  143.   text-decoration: underline;
  144. }
  145. </style>
复制代码

在src/views/Tasks.vue中创建任务列表页面:
  1. <template>
  2.   <div class="tasks-container">
  3.     <div class="tasks-header">
  4.       <h1>Tasks</h1>
  5.       <router-link to="/tasks/create" class="btn-create">Create Task</router-link>
  6.     </div>
  7.    
  8.     <div v-if="loading" class="loading">Loading tasks...</div>
  9.    
  10.     <div v-else-if="error" class="error">{{ error }}</div>
  11.    
  12.     <div v-else-if="tasks.length === 0" class="empty-state">
  13.       <p>No tasks found. Create your first task!</p>
  14.       <router-link to="/tasks/create" class="btn-create">Create Task</router-link>
  15.     </div>
  16.    
  17.     <div v-else class="tasks-list">
  18.       <div v-for="task in tasks" :key="task.id" class="task-card">
  19.         <div class="task-header">
  20.           <h3>{{ task.title }}</h3>
  21.           <span :class="['priority', task.priority]">{{ task.priority }}</span>
  22.         </div>
  23.         <p class="task-description">{{ task.description || 'No description' }}</p>
  24.         <div class="task-meta">
  25.           <span :class="['status', task.status]">{{ task.status }}</span>
  26.           <span class="due-date" v-if="task.due_date">
  27.             Due: {{ formatDate(task.due_date) }}
  28.           </span>
  29.         </div>
  30.         <div class="task-actions">
  31.           <router-link :to="`/tasks/${task.id}`" class="btn-view">View</router-link>
  32.           <router-link :to="`/tasks/${task.id}/edit`" class="btn-edit">Edit</router-link>
  33.           <button @click="confirmDelete(task)" class="btn-delete">Delete</button>
  34.         </div>
  35.       </div>
  36.     </div>
  37.    
  38.     <div v-if="showDeleteModal" class="modal">
  39.       <div class="modal-content">
  40.         <h3>Confirm Delete</h3>
  41.         <p>Are you sure you want to delete "{{ taskToDelete?.title }}"?</p>
  42.         <div class="modal-actions">
  43.           <button @click="cancelDelete" class="btn-cancel">Cancel</button>
  44.           <button @click="deleteTask" class="btn-confirm" :disabled="deleting">
  45.             {{ deleting ? 'Deleting...' : 'Delete' }}
  46.           </button>
  47.         </div>
  48.       </div>
  49.     </div>
  50.   </div>
  51. </template>
  52. <script>
  53. import { ref, onMounted } from 'vue';
  54. import { useRouter } from 'vue-router';
  55. import { useTaskStore } from '@/stores/task';
  56. export default {
  57.   name: 'Tasks',
  58.   setup() {
  59.     const router = useRouter();
  60.     const taskStore = useTaskStore();
  61.    
  62.     const tasks = ref([]);
  63.     const loading = ref(false);
  64.     const error = ref(null);
  65.    
  66.     const showDeleteModal = ref(false);
  67.     const taskToDelete = ref(null);
  68.     const deleting = ref(false);
  69.    
  70.     const loadTasks = async () => {
  71.       loading.value = true;
  72.       error.value = null;
  73.       
  74.       try {
  75.         await taskStore.fetchTasks();
  76.         tasks.value = taskStore.getAllTasks;
  77.       } catch (err) {
  78.         error.value = err.message || 'Failed to load tasks';
  79.       } finally {
  80.         loading.value = false;
  81.       }
  82.     };
  83.    
  84.     const formatDate = (dateString) => {
  85.       const options = { year: 'numeric', month: 'short', day: 'numeric' };
  86.       return new Date(dateString).toLocaleDateString(undefined, options);
  87.     };
  88.    
  89.     const confirmDelete = (task) => {
  90.       taskToDelete.value = task;
  91.       showDeleteModal.value = true;
  92.     };
  93.    
  94.     const cancelDelete = () => {
  95.       showDeleteModal.value = false;
  96.       taskToDelete.value = null;
  97.     };
  98.    
  99.     const deleteTask = async () => {
  100.       if (!taskToDelete.value) return;
  101.       
  102.       deleting.value = true;
  103.       
  104.       try {
  105.         await taskStore.deleteTask(taskToDelete.value.id);
  106.         tasks.value = taskStore.getAllTasks;
  107.         showDeleteModal.value = false;
  108.         taskToDelete.value = null;
  109.       } catch (err) {
  110.         error.value = err.message || 'Failed to delete task';
  111.       } finally {
  112.         deleting.value = false;
  113.       }
  114.     };
  115.    
  116.     onMounted(() => {
  117.       loadTasks();
  118.     });
  119.    
  120.     return {
  121.       tasks,
  122.       loading,
  123.       error,
  124.       showDeleteModal,
  125.       taskToDelete,
  126.       deleting,
  127.       formatDate,
  128.       confirmDelete,
  129.       cancelDelete,
  130.       deleteTask,
  131.     };
  132.   },
  133. };
  134. </script>
  135. <style scoped>
  136. .tasks-container {
  137.   max-width: 1200px;
  138.   margin: 0 auto;
  139.   padding: 2rem;
  140. }
  141. .tasks-header {
  142.   display: flex;
  143.   justify-content: space-between;
  144.   align-items: center;
  145.   margin-bottom: 2rem;
  146. }
  147. .btn-create {
  148.   display: inline-block;
  149.   padding: 0.75rem 1.5rem;
  150.   background-color: #4caf50;
  151.   color: white;
  152.   text-decoration: none;
  153.   border-radius: 4px;
  154.   font-weight: bold;
  155.   transition: background-color 0.3s;
  156. }
  157. .btn-create:hover {
  158.   background-color: #45a049;
  159. }
  160. .loading, .error, .empty-state {
  161.   text-align: center;
  162.   padding: 2rem;
  163.   font-size: 1.2rem;
  164. }
  165. .error {
  166.   color: #f44336;
  167. }
  168. .tasks-list {
  169.   display: grid;
  170.   grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  171.   gap: 1.5rem;
  172. }
  173. .task-card {
  174.   background-color: white;
  175.   border-radius: 8px;
  176.   box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  177.   padding: 1.5rem;
  178.   display: flex;
  179.   flex-direction: column;
  180. }
  181. .task-header {
  182.   display: flex;
  183.   justify-content: space-between;
  184.   align-items: center;
  185.   margin-bottom: 1rem;
  186. }
  187. .task-header h3 {
  188.   margin: 0;
  189.   font-size: 1.25rem;
  190. }
  191. .priority {
  192.   padding: 0.25rem 0.5rem;
  193.   border-radius: 4px;
  194.   font-size: 0.75rem;
  195.   font-weight: bold;
  196.   text-transform: uppercase;
  197. }
  198. .priority.low {
  199.   background-color: #e8f5e9;
  200.   color: #2e7d32;
  201. }
  202. .priority.medium {
  203.   background-color: #fff8e1;
  204.   color: #ff8f00;
  205. }
  206. .priority.high {
  207.   background-color: #ffebee;
  208.   color: #c62828;
  209. }
  210. .task-description {
  211.   color: #666;
  212.   margin-bottom: 1rem;
  213.   flex-grow: 1;
  214. }
  215. .task-meta {
  216.   display: flex;
  217.   justify-content: space-between;
  218.   margin-bottom: 1rem;
  219.   font-size: 0.875rem;
  220. }
  221. .status {
  222.   padding: 0.25rem 0.5rem;
  223.   border-radius: 4px;
  224.   font-weight: bold;
  225.   text-transform: capitalize;
  226. }
  227. .status.pending {
  228.   background-color: #e3f2fd;
  229.   color: #1565c0;
  230. }
  231. .status.in_progress {
  232.   background-color: #fff8e1;
  233.   color: #ff8f00;
  234. }
  235. .status.completed {
  236.   background-color: #e8f5e9;
  237.   color: #2e7d32;
  238. }
  239. .due-date {
  240.   color: #666;
  241. }
  242. .task-actions {
  243.   display: flex;
  244.   gap: 0.5rem;
  245. }
  246. .btn-view, .btn-edit {
  247.   display: inline-block;
  248.   padding: 0.5rem 1rem;
  249.   text-decoration: none;
  250.   border-radius: 4px;
  251.   font-size: 0.875rem;
  252.   transition: background-color 0.3s;
  253. }
  254. .btn-view {
  255.   background-color: #e3f2fd;
  256.   color: #1565c0;
  257. }
  258. .btn-view:hover {
  259.   background-color: #bbdefb;
  260. }
  261. .btn-edit {
  262.   background-color: #fff8e1;
  263.   color: #ff8f00;
  264. }
  265. .btn-edit:hover {
  266.   background-color: #ffecb3;
  267. }
  268. .btn-delete {
  269.   padding: 0.5rem 1rem;
  270.   background-color: #ffebee;
  271.   color: #c62828;
  272.   border: none;
  273.   border-radius: 4px;
  274.   font-size: 0.875rem;
  275.   cursor: pointer;
  276.   transition: background-color 0.3s;
  277. }
  278. .btn-delete:hover {
  279.   background-color: #ffcdd2;
  280. }
  281. .btn-delete:disabled {
  282.   background-color: #f5f5f5;
  283.   color: #999;
  284.   cursor: not-allowed;
  285. }
  286. .modal {
  287.   position: fixed;
  288.   top: 0;
  289.   left: 0;
  290.   right: 0;
  291.   bottom: 0;
  292.   background-color: rgba(0, 0, 0, 0.5);
  293.   display: flex;
  294.   justify-content: center;
  295.   align-items: center;
  296.   z-index: 1000;
  297. }
  298. .modal-content {
  299.   background-color: white;
  300.   padding: 2rem;
  301.   border-radius: 8px;
  302.   max-width: 500px;
  303.   width: 100%;
  304. }
  305. .modal-content h3 {
  306.   margin-top: 0;
  307.   margin-bottom: 1rem;
  308. }
  309. .modal-actions {
  310.   display: flex;
  311.   justify-content: flex-end;
  312.   gap: 1rem;
  313.   margin-top: 1.5rem;
  314. }
  315. .btn-cancel, .btn-confirm {
  316.   padding: 0.75rem 1.5rem;
  317.   border: none;
  318.   border-radius: 4px;
  319.   font-weight: bold;
  320.   cursor: pointer;
  321.   transition: background-color 0.3s;
  322. }
  323. .btn-cancel {
  324.   background-color: #f5f5f5;
  325.   color: #333;
  326. }
  327. .btn-cancel:hover {
  328.   background-color: #e0e0e0;
  329. }
  330. .btn-confirm {
  331.   background-color: #f44336;
  332.   color: white;
  333. }
  334. .btn-confirm:hover {
  335.   background-color: #d32f2f;
  336. }
  337. .btn-confirm:disabled {
  338.   background-color: #ffcdd2;
  339.   color: white;
  340.   cursor: not-allowed;
  341. }
  342. </style>
复制代码

在src/views/CreateTask.vue中创建任务创建页面:
  1. <template>
  2.   <div class="create-task-container">
  3.     <h1>Create Task</h1>
  4.     <form @submit.prevent="handleSubmit" class="task-form">
  5.       <div class="form-group">
  6.         <label for="title">Title *</label>
  7.         <input
  8.           type="text"
  9.           id="title"
  10.           v-model="task.title"
  11.           required
  12.         />
  13.       </div>
  14.       
  15.       <div class="form-group">
  16.         <label for="description">Description</label>
  17.         <textarea
  18.           id="description"
  19.           v-model="task.description"
  20.           rows="4"
  21.         ></textarea>
  22.       </div>
  23.       
  24.       <div class="form-group">
  25.         <label for="status">Status</label>
  26.         <select id="status" v-model="task.status">
  27.           <option value="pending">Pending</option>
  28.           <option value="in_progress">In Progress</option>
  29.           <option value="completed">Completed</option>
  30.         </select>
  31.       </div>
  32.       
  33.       <div class="form-group">
  34.         <label for="priority">Priority</label>
  35.         <select id="priority" v-model="task.priority">
  36.           <option value="low">Low</option>
  37.           <option value="medium">Medium</option>
  38.           <option value="high">High</option>
  39.         </select>
  40.       </div>
  41.       
  42.       <div class="form-group">
  43.         <label for="due_date">Due Date</label>
  44.         <input
  45.           type="date"
  46.           id="due_date"
  47.           v-model="task.due_date"
  48.         />
  49.       </div>
  50.       
  51.       <div v-if="error" class="error-message">{{ error }}</div>
  52.       
  53.       <div class="form-actions">
  54.         <button type="button" @click="goBack" class="btn-cancel">Cancel</button>
  55.         <button type="submit" class="btn-submit" :disabled="loading">
  56.           {{ loading ? 'Creating...' : 'Create Task' }}
  57.         </button>
  58.       </div>
  59.     </form>
  60.   </div>
  61. </template>
  62. <script>
  63. import { ref, reactive } from 'vue';
  64. import { useRouter } from 'vue-router';
  65. import { useTaskStore } from '@/stores/task';
  66. export default {
  67.   name: 'CreateTask',
  68.   setup() {
  69.     const router = useRouter();
  70.     const taskStore = useTaskStore();
  71.    
  72.     const task = reactive({
  73.       title: '',
  74.       description: '',
  75.       status: 'pending',
  76.       priority: 'medium',
  77.       due_date: '',
  78.     });
  79.    
  80.     const loading = ref(false);
  81.     const error = ref(null);
  82.    
  83.     const handleSubmit = async () => {
  84.       loading.value = true;
  85.       error.value = null;
  86.       
  87.       try {
  88.         await taskStore.createTask(task);
  89.         router.push('/tasks');
  90.       } catch (err) {
  91.         error.value = err.message || 'Failed to create task';
  92.       } finally {
  93.         loading.value = false;
  94.       }
  95.     };
  96.    
  97.     const goBack = () => {
  98.       router.push('/tasks');
  99.     };
  100.    
  101.     return {
  102.       task,
  103.       loading,
  104.       error,
  105.       handleSubmit,
  106.       goBack,
  107.     };
  108.   },
  109. };
  110. </script>
  111. <style scoped>
  112. .create-task-container {
  113.   max-width: 800px;
  114.   margin: 0 auto;
  115.   padding: 2rem;
  116. }
  117. h1 {
  118.   margin-bottom: 2rem;
  119. }
  120. .task-form {
  121.   background-color: white;
  122.   padding: 2rem;
  123.   border-radius: 8px;
  124.   box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  125. }
  126. .form-group {
  127.   margin-bottom: 1.5rem;
  128. }
  129. label {
  130.   display: block;
  131.   margin-bottom: 0.5rem;
  132.   font-weight: bold;
  133. }
  134. input[type="text"],
  135. input[type="date"],
  136. textarea,
  137. select {
  138.   width: 100%;
  139.   padding: 0.75rem;
  140.   border: 1px solid #ddd;
  141.   border-radius: 4px;
  142.   font-size: 1rem;
  143. }
  144. textarea {
  145.   resize: vertical;
  146. }
  147. .error-message {
  148.   color: #f44336;
  149.   margin-bottom: 1rem;
  150. }
  151. .form-actions {
  152.   display: flex;
  153.   justify-content: flex-end;
  154.   gap: 1rem;
  155. }
  156. .btn-cancel, .btn-submit {
  157.   padding: 0.75rem 1.5rem;
  158.   border: none;
  159.   border-radius: 4px;
  160.   font-weight: bold;
  161.   cursor: pointer;
  162.   transition: background-color 0.3s;
  163. }
  164. .btn-cancel {
  165.   background-color: #f5f5f5;
  166.   color: #333;
  167. }
  168. .btn-cancel:hover {
  169.   background-color: #e0e0e0;
  170. }
  171. .btn-submit {
  172.   background-color: #4caf50;
  173.   color: white;
  174. }
  175. .btn-submit:hover {
  176.   background-color: #45a049;
  177. }
  178. .btn-submit:disabled {
  179.   background-color: #cccccc;
  180.   cursor: not-allowed;
  181. }
  182. </style>
复制代码

主应用文件

在src/App.vue中:
  1. <template>
  2.   <div id="app">
  3.     <nav class="navbar" v-if="authStore.isAuthenticated">
  4.       <div class="nav-container">
  5.         <router-link to="/" class="nav-logo">Task Manager</router-link>
  6.         <div class="nav-menu">
  7.           <router-link to="/" class="nav-link">Home</router-link>
  8.           <router-link to="/tasks" class="nav-link">Tasks</router-link>
  9.           <a @click="handleLogout" class="nav-link logout">Logout</a>
  10.         </div>
  11.       </div>
  12.     </nav>
  13.    
  14.     <main class="main-content">
  15.       <router-view />
  16.     </main>
  17.    
  18.     <footer class="footer">
  19.       <p>&copy; {{ currentYear }} Task Manager. All rights reserved.</p>
  20.     </footer>
  21.   </div>
  22. </template>
  23. <script>
  24. import { computed } from 'vue';
  25. import { useRouter } from 'vue-router';
  26. import { useAuthStore } from '@/stores/auth';
  27. export default {
  28.   name: 'App',
  29.   setup() {
  30.     const router = useRouter();
  31.     const authStore = useAuthStore();
  32.    
  33.     const currentYear = computed(() => new Date().getFullYear());
  34.    
  35.     const handleLogout = async () => {
  36.       await authStore.logout();
  37.       router.push('/login');
  38.     };
  39.    
  40.     return {
  41.       authStore,
  42.       currentYear,
  43.       handleLogout,
  44.     };
  45.   },
  46. };
  47. </script>
  48. <style>
  49. * {
  50.   margin: 0;
  51.   padding: 0;
  52.   box-sizing: border-box;
  53. }
  54. body {
  55.   font-family: 'Arial', sans-serif;
  56.   line-height: 1.6;
  57.   color: #333;
  58.   background-color: #f5f5f5;
  59. }
  60. #app {
  61.   display: flex;
  62.   flex-direction: column;
  63.   min-height: 100vh;
  64. }
  65. .navbar {
  66.   background-color: #2c3e50;
  67.   color: white;
  68.   padding: 1rem 0;
  69.   box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  70. }
  71. .nav-container {
  72.   max-width: 1200px;
  73.   margin: 0 auto;
  74.   padding: 0 2rem;
  75.   display: flex;
  76.   justify-content: space-between;
  77.   align-items: center;
  78. }
  79. .nav-logo {
  80.   font-size: 1.5rem;
  81.   font-weight: bold;
  82.   color: white;
  83.   text-decoration: none;
  84. }
  85. .nav-menu {
  86.   display: flex;
  87.   gap: 1.5rem;
  88. }
  89. .nav-link {
  90.   color: white;
  91.   text-decoration: none;
  92.   font-weight: 500;
  93.   transition: color 0.3s;
  94. }
  95. .nav-link:hover {
  96.   color: #3498db;
  97. }
  98. .logout {
  99.   cursor: pointer;
  100. }
  101. .main-content {
  102.   flex: 1;
  103.   padding: 2rem 0;
  104. }
  105. .footer {
  106.   background-color: #2c3e50;
  107.   color: white;
  108.   text-align: center;
  109.   padding: 1rem 0;
  110.   margin-top: auto;
  111. }
  112. </style>
复制代码

在src/main.js中:
  1. import { createApp } from 'vue';
  2. import { createPinia } from 'pinia';
  3. import App from './App.vue';
  4. import router from './router';
  5. const app = createApp(App);
  6. app.use(createPinia());
  7. app.use(router);
  8. app.mount('#app');
复制代码

前后端交互实战

用户认证流程

我们已经实现了完整的用户认证流程,包括注册、登录和令牌刷新。以下是认证流程的工作原理:

1. 注册:用户提交注册表单后,前端将用户数据发送到Django的/api/auth/register/端点。Django创建新用户并返回访问令牌和刷新令牌。前端将这些令牌存储在localStorage中,并更新Pinia store中的认证状态。
2. 登录:用户提交登录表单后,前端将凭据发送到Django的/api/auth/login/端点。Django验证凭据并返回访问令牌和刷新令牌。前端同样将这些令牌存储在localStorage中,并更新认证状态。
3. 令牌刷新:访问令牌通常有较短的过期时间(在我们的示例中为60分钟),而刷新令牌有较长的过期时间(1天)。当访问令牌过期时,前端会自动使用刷新令牌请求新的访问令牌。如果刷新令牌也过期或无效,用户将被重定向到登录页面。
4. 请求拦截:所有API请求都会通过axios拦截器自动添加Authorization头,包含访问令牌。如果请求返回401未授权错误,拦截器会尝试使用刷新令牌获取新的访问令牌,然后重新发送原始请求。
5. 路由守卫:Vue Router的导航守卫检查用户是否已认证,并根据路由的meta信息决定是否允许访问需要认证的页面或重定向到登录页面。

注册:用户提交注册表单后,前端将用户数据发送到Django的/api/auth/register/端点。Django创建新用户并返回访问令牌和刷新令牌。前端将这些令牌存储在localStorage中,并更新Pinia store中的认证状态。

登录:用户提交登录表单后,前端将凭据发送到Django的/api/auth/login/端点。Django验证凭据并返回访问令牌和刷新令牌。前端同样将这些令牌存储在localStorage中,并更新认证状态。

令牌刷新:访问令牌通常有较短的过期时间(在我们的示例中为60分钟),而刷新令牌有较长的过期时间(1天)。当访问令牌过期时,前端会自动使用刷新令牌请求新的访问令牌。如果刷新令牌也过期或无效,用户将被重定向到登录页面。

请求拦截:所有API请求都会通过axios拦截器自动添加Authorization头,包含访问令牌。如果请求返回401未授权错误,拦截器会尝试使用刷新令牌获取新的访问令牌,然后重新发送原始请求。

路由守卫:Vue Router的导航守卫检查用户是否已认证,并根据路由的meta信息决定是否允许访问需要认证的页面或重定向到登录页面。

CRUD操作示例

我们的任务管理系统实现了完整的CRUD(创建、读取、更新、删除)操作:

1. 创建任务:用户可以通过”Create Task”页面创建新任务。表单数据被发送到Django的/api/tasks/端点,Django创建新任务并返回任务数据。前端将新任务添加到任务列表中。
2. 读取任务:任务列表页面从Django的/api/tasks/端点获取所有任务。用户可以点击任务查看详细信息,这会从/api/tasks/{id}/端点获取特定任务的数据。
3. 更新任务:用户可以通过”Edit Task”页面编辑现有任务。更新后的数据被发送到Django的/api/tasks/{id}/端点,Django更新任务并返回更新后的数据。前端更新任务列表中的相应任务。
4. 删除任务:用户可以删除任务,这会向Django的/api/tasks/{id}/端点发送DELETE请求。Django删除任务并返回成功响应。前端从任务列表中移除已删除的任务。

创建任务:用户可以通过”Create Task”页面创建新任务。表单数据被发送到Django的/api/tasks/端点,Django创建新任务并返回任务数据。前端将新任务添加到任务列表中。

读取任务:任务列表页面从Django的/api/tasks/端点获取所有任务。用户可以点击任务查看详细信息,这会从/api/tasks/{id}/端点获取特定任务的数据。

更新任务:用户可以通过”Edit Task”页面编辑现有任务。更新后的数据被发送到Django的/api/tasks/{id}/端点,Django更新任务并返回更新后的数据。前端更新任务列表中的相应任务。

删除任务:用户可以删除任务,这会向Django的/api/tasks/{id}/端点发送DELETE请求。Django删除任务并返回成功响应。前端从任务列表中移除已删除的任务。

数据状态同步

我们的应用使用Pinia进行状态管理,确保前后端数据同步:

1. 任务状态:useTaskStore管理所有任务的状态,包括任务列表、当前任务、加载状态和错误信息。当执行CRUD操作时,store会自动更新,确保UI与后端数据保持同步。
2. 认证状态:useAuthStore管理用户认证状态,包括用户信息、访问令牌、刷新令牌和认证状态。当用户登录或注销时,store会更新,并相应地更新localStorage和UI。
3. 响应式更新:由于Pinia的响应式特性,当store中的状态发生变化时,所有依赖这些状态的组件都会自动更新,确保UI始终反映最新的数据状态。
4. 错误处理:store中的错误处理机制确保当API请求失败时,错误信息能够正确显示给用户,同时不会破坏应用的状态。

任务状态:useTaskStore管理所有任务的状态,包括任务列表、当前任务、加载状态和错误信息。当执行CRUD操作时,store会自动更新,确保UI与后端数据保持同步。

认证状态:useAuthStore管理用户认证状态,包括用户信息、访问令牌、刷新令牌和认证状态。当用户登录或注销时,store会更新,并相应地更新localStorage和UI。

响应式更新:由于Pinia的响应式特性,当store中的状态发生变化时,所有依赖这些状态的组件都会自动更新,确保UI始终反映最新的数据状态。

错误处理:store中的错误处理机制确保当API请求失败时,错误信息能够正确显示给用户,同时不会破坏应用的状态。

安全最佳实践

在构建前后端分离应用时,安全性是至关重要的。以下是我们实现的一些安全最佳实践:

后端安全措施

1. JWT认证:使用JSON Web Tokens (JWT)进行用户认证,而不是传统的会话认证。这使得我们的API可以无状态地工作,更适合前后端分离的架构。
2. CORS配置:正确配置跨域资源共享(CORS),只允许来自我们前端域名的请求,防止跨站请求伪造(CSRF)攻击。
3. 令牌过期:设置合理的令牌过期时间,访问令牌设置为60分钟,刷新令牌设置为1天,以减少令牌被滥用的风险。
4. HTTPS:在生产环境中,确保使用HTTPS协议加密所有通信,防止中间人攻击。
5. 数据验证:在Django中使用序列化器进行数据验证,确保所有输入数据都符合预期格式,防止注入攻击。
6. 权限控制:使用Django REST framework的权限系统,确保用户只能访问和修改自己的任务数据。

JWT认证:使用JSON Web Tokens (JWT)进行用户认证,而不是传统的会话认证。这使得我们的API可以无状态地工作,更适合前后端分离的架构。

CORS配置:正确配置跨域资源共享(CORS),只允许来自我们前端域名的请求,防止跨站请求伪造(CSRF)攻击。

令牌过期:设置合理的令牌过期时间,访问令牌设置为60分钟,刷新令牌设置为1天,以减少令牌被滥用的风险。

HTTPS:在生产环境中,确保使用HTTPS协议加密所有通信,防止中间人攻击。

数据验证:在Django中使用序列化器进行数据验证,确保所有输入数据都符合预期格式,防止注入攻击。

权限控制:使用Django REST framework的权限系统,确保用户只能访问和修改自己的任务数据。

前端安全措施

1. 令牌存储:将JWT令牌存储在localStorage中,而不是cookie中,以防止跨站脚本攻击(XSS)窃取令牌。注意:这种方法也有其自身的安全考虑,在生产环境中可能需要更安全的解决方案,如使用HttpOnly cookie。
2. 路由守卫:使用Vue Router的导航守卫保护需要认证的路由,防止未认证用户访问受限页面。
3. 请求拦截:使用axios拦截器自动添加认证头,确保所有API请求都包含有效的访问令牌。
4. 输入验证:在前端表单中进行基本的输入验证,提供即时反馈,但记住前端验证不能替代后端验证。
5. 错误处理:妥善处理API错误,避免在UI中暴露敏感信息。

令牌存储:将JWT令牌存储在localStorage中,而不是cookie中,以防止跨站脚本攻击(XSS)窃取令牌。注意:这种方法也有其自身的安全考虑,在生产环境中可能需要更安全的解决方案,如使用HttpOnly cookie。

路由守卫:使用Vue Router的导航守卫保护需要认证的路由,防止未认证用户访问受限页面。

请求拦截:使用axios拦截器自动添加认证头,确保所有API请求都包含有效的访问令牌。

输入验证:在前端表单中进行基本的输入验证,提供即时反馈,但记住前端验证不能替代后端验证。

错误处理:妥善处理API错误,避免在UI中暴露敏感信息。

部署考虑

虽然本文主要关注开发过程,但了解部署考虑也很重要:

后端部署

1. 生产环境设置:在Django的settings.py中设置DEBUG = False,并配置ALLOWED_HOSTS。
2. 数据库:使用生产级数据库如PostgreSQL或MySQL,而不是开发用的SQLite。
3. 静态文件:配置静态文件服务,使用WhiteNoise或类似服务,或使用CDN。
4. WSGI服务器:使用Gunicorn或uWSGI作为应用服务器,而不是Django的开发服务器。
5. 反向代理:使用Nginx或Apache作为反向代理,处理静态文件、SSL终止和请求转发。

生产环境设置:在Django的settings.py中设置DEBUG = False,并配置ALLOWED_HOSTS。

数据库:使用生产级数据库如PostgreSQL或MySQL,而不是开发用的SQLite。

静态文件:配置静态文件服务,使用WhiteNoise或类似服务,或使用CDN。

WSGI服务器:使用Gunicorn或uWSGI作为应用服务器,而不是Django的开发服务器。

反向代理:使用Nginx或Apache作为反向代理,处理静态文件、SSL终止和请求转发。

前端部署

1. 构建优化:使用Vue CLI的构建命令生成优化后的生产版本代码:
  1. npm run build
复制代码

1. 静态文件服务:将构建后的静态文件部署到Web服务器或CDN。
2. 环境变量:使用环境变量配置API基础URL等设置,避免硬编码。
3. 缓存策略:配置适当的缓存策略,平衡性能和更新需求。

静态文件服务:将构建后的静态文件部署到Web服务器或CDN。

环境变量:使用环境变量配置API基础URL等设置,避免硬编码。

缓存策略:配置适当的缓存策略,平衡性能和更新需求。

CI/CD

考虑设置持续集成/持续部署(CI/CD)流程,自动化测试和部署过程,确保代码质量和部署可靠性。

总结与展望

本文详细介绍了如何使用Vue3和Django构建一个完整的前后端分离应用,涵盖了从环境搭建、API设计、数据通信、状态管理到安全认证的完整流程。通过这个实战项目,我们学习了:

1. Django REST framework的使用,包括序列化器、视图集和路由器的配置。
2. JWT认证的实现,包括令牌生成、验证和刷新。
3. Vue3组件化开发,使用Composition API构建响应式UI。
4. Pinia状态管理,实现前后端数据同步。
5. Vue Router路由管理,包括导航守卫和路由参数。
6. Axios封装,实现请求拦截和错误处理。
7. 前后端交互的最佳实践,包括CORS配置和安全考虑。

这个项目可以作为更复杂应用的基础,你可以根据需要扩展功能,例如:

1. 添加实时通知功能,使用WebSocket或Server-Sent Events。
2. 实现更复杂的权限系统,如基于角色的访问控制。
3. 添加文件上传功能,允许用户为任务添加附件。
4. 实现任务搜索和过滤功能。
5. 添加数据可视化,使用图表库展示任务统计信息。
6. 实现国际化支持,使应用支持多语言。

随着Web技术的不断发展,前后端分离架构将继续演进,新的工具和框架也会不断涌现。但无论技术如何变化,理解基本原理和最佳实践将帮助你构建更安全、更可靠、更易维护的Web应用。

希望本文能为你在Vue3和Django前后端分离开发之旅中提供有价值的指导和参考。祝你编码愉快!
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

频道订阅

频道订阅

加入社群

加入社群

联系我们|TG频道|RSS

Powered by Pixtech

© 2025 Pixtech Team.