Python Web 开发进阶实战:前端现代化 —— Vue 3 + TypeScript 重构 Layui 界面,打造高性能 SPA

第一章:为什么需要前端现代化?

1.1 Layui 的局限性

问题 后果
非组件化 代码重复、难以复用(如多个表单校验逻辑)
无状态管理 多页面间数据共享困难(如用户信息)
jQuery 风格 DOM 操作繁琐,易出错
无构建工具 无法使用 ES6+、TypeScript、CSS 预处理器
体验割裂 每次操作都整页刷新

1.2 Vue 3 的优势

  • 组合式 API(Composition API):逻辑复用更清晰
  • 响应式系统:自动更新 UI,无需手动操作 DOM
  • TypeScript 一流支持:类型安全,减少运行时错误
  • Vite 极速构建:冷启动 < 500ms,HMR 瞬时更新
  • 生态丰富:Pinia(状态管理)、Vue Router(路由)、Naive UI(组件库)

技术选型

  • 框架:Vue 3( 语法)
  • 语言:TypeScript
  • 构建工具:Vite
  • 状态管理:Pinia
  • UI 组件库:Naive UI(轻量、TypeScript 友好、支持暗色主题)
  • HTTP 客户端:Axios(带拦截器)

第二章:搭建 Vue 3 + TypeScript 项目

2.1 创建项目

复制代码
npm create vue@latest myapp-frontend

选择:

  • ✔ TypeScript
  • ✔ Pinia
  • ✔ Vue Router
  • ✘ ESLint / Prettier(可后续添加)

进入目录并安装依赖:

复制代码
cd myapp-frontend
npm install

2.2 集成 Naive UI

复制代码
npm install naive-ui

main.ts 中全局注册(按需引入以减小体积):

复制代码
// main.ts
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import { createPinia } from 'pinia'

// Naive UI
import { createDiscreteApi } from 'naive-ui'
const { message, dialog, notification } = createDiscreteApi(['message', 'dialog', 'notification'])

const app = createApp(App)
app.use(createPinia())
app.use(router)

// 挂载全局 API(用于组件内调用)
app.config.globalProperties.$message = message
app.config.globalProperties.$dialog = dialog
app.config.globalProperties.$notification = notification

app.mount('#app')

注意 :实际项目建议使用 插件方式provide/inject,此处为简化演示。

2.3 配置 Vite 代理(开发环境)

避免跨域问题,在 vite.config.ts 中配置:

复制代码
// vite.config.ts
export default defineConfig({
  plugins: [vue()],
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:5000', // Flask 开发服务器
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, '')
      }
    }
  }
})

所有 /api/xxx 请求将代理到 http://localhost:5000/xxx


第三章:API 封装与 Axios 拦截器

3.1 创建 API 客户端

新建 src/api/client.ts

复制代码
// src/api/client.ts
import axios from 'axios'

const apiClient = axios.create({
  baseURL: import.meta.env.DEV ? '/api' : '/api', // 生产环境同域
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json'
  }
})

export default apiClient

3.2 JWT 认证拦截器

新建 src/plugins/axios.ts

复制代码
// src/plugins/axios.ts
import type { AxiosRequestConfig, InternalAxiosRequestConfig } from 'axios'
import apiClient from '@/api/client'
import { useAuthStore } from '@/stores/auth'
import { refreshToken } from '@/api/auth'

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

// 响应拦截器:处理 401 与 Token 刷新
let isRefreshing = false
let failedQueue: Array<{
  resolve: (value?: unknown) => void
  reject: (reason?: unknown) => void
}> = []

apiClient.interceptors.response.use(
  (response) => response,
  async (error) => {
    const originalRequest = error.config as AxiosRequestConfig & { _retry?: boolean }
    
    if (error.response?.status === 401 && !originalRequest._retry) {
      if (isRefreshing) {
        // 等待刷新完成
        return new Promise((resolve, reject) => {
          failedQueue.push({ resolve, reject })
        }).then(() => apiClient(originalRequest))
      }

      originalRequest._retry = true
      isRefreshing = true

      try {
        const authStore = useAuthStore()
        await refreshToken(authStore.refreshToken!)
        isRefreshing = false

        // 重试队列中的请求
        failedQueue.forEach(({ resolve }) => resolve(apiClient(originalRequest)))
        failedQueue = []
        
        return apiClient(originalRequest)
      } catch (refreshError) {
        // 刷新失败,登出
        const authStore = useAuthStore()
        authStore.logout()
        failedQueue.forEach(({ reject }) => reject(refreshError))
        failedQueue = []
        return Promise.reject(refreshError)
      }
    }
    return Promise.reject(error)
  }
)

关键逻辑

  • 自动附加 Authorization
  • 拦截 401,用 refresh_token 获取新 access_token
  • 并发请求排队,避免多次刷新

main.ts 中导入该插件:

复制代码
// main.ts
import '@/plugins/axios' // ← 添加此行

第四章:状态管理 ------ Pinia 用户认证模块

4.1 创建 Auth Store

新建 src/stores/auth.ts

复制代码
// src/stores/auth.ts
import { defineStore } from 'pinia'
import { login as apiLogin, logout as apiLogout } from '@/api/auth'
import type { LoginData } from '@/types'

interface AuthState {
  accessToken: string | null
  refreshToken: string | null
  userId: number | null
  username: string | null
}

export const useAuthStore = defineStore('auth', {
  state: (): AuthState => ({
    accessToken: localStorage.getItem('access_token'),
    refreshToken: localStorage.getItem('refresh_token'),
    userId: parseInt(localStorage.getItem('user_id') || '0') || null,
    username: localStorage.getItem('username')
  }),

  getters: {
    isAuthenticated: (state) => !!state.accessToken,
    currentUsername: (state) => state.username
  },

  actions: {
    async login(credentials: LoginData) {
      const response = await apiLogin(credentials)
      const { access_token, refresh_token, user } = response.data
      
      this.accessToken = access_token
      this.refreshToken = refresh_token
      this.userId = user.id
      this.username = user.username

      // 持久化
      localStorage.setItem('access_token', access_token)
      localStorage.setItem('refresh_token', refresh_token)
      localStorage.setItem('user_id', user.id.toString())
      localStorage.setItem('username', user.username)
    },

    logout() {
      apiLogout() // 可选:通知后端失效 Token
      this.$reset()
      localStorage.clear()
    }
  }
})

4.2 类型定义

新建 src/types/index.ts

复制代码
// src/types/index.ts
export interface User {
  id: number
  username: string
  email: string
}

export interface LoginData {
  username: string
  password: string
}

export interface LoginResponse {
  access_token: string
  refresh_token: string
  user: User
}

第五章:登录与注册页面实现

5.1 路由配置

更新 src/router/index.ts

复制代码
// src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import { useAuthStore } from '@/stores/auth'

const routes = [
  {
    path: '/login',
    name: 'Login',
    component: () => import('@/views/LoginView.vue'),
    meta: { guestOnly: true }
  },
  {
    path: '/register',
    name: 'Register',
    component: () => import('@/views/RegisterView.vue'),
    meta: { guestOnly: true }
  },
  {
    path: '/',
    component: () => import('@/layouts/MainLayout.vue'),
    meta: { requiresAuth: true },
    children: [
      { path: '', name: 'Dashboard', component: () => import('@/views/DashboardView.vue') },
      { path: '/profile', name: 'Profile', component: () => import('@/views/ProfileView.vue') }
    ]
  }
]

const router = createRouter({
  history: createWebHistory(),
  routes
})

// 路由守卫
router.beforeEach((to, from, next) => {
  const authStore = useAuthStore()

  if (to.meta.requiresAuth && !authStore.isAuthenticated) {
    next('/login')
  } else if (to.meta.guestOnly && authStore.isAuthenticated) {
    next('/')
  } else {
    next()
  }
})

export default router

5.2 LoginView.vue

复制代码
<!-- src/views/LoginView.vue -->
<template>
  <div class="login-container">
    <n-card title="登录" style="max-width: 400px; margin: 0 auto;">
      <n-form :model="form" :rules="rules" ref="formRef">
        <n-form-item label="用户名" path="username">
          <n-input v-model:value="form.username" placeholder="请输入用户名" />
        </n-form-item>
        <n-form-item label="密码" path="password">
          <n-input type="password" v-model:value="form.password" placeholder="请输入密码" />
        </n-form-item>
        <n-button type="primary" @click="handleLogin" block :loading="loading">
          登录
        </n-button>
      </n-form>
    </n-card>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { useMessage } from 'naive-ui'
import { useAuthStore } from '@/stores/auth'
import type { FormInst } from 'naive-ui'

const formRef = ref<FormInst | null>(null)
const form = ref({ username: '', password: '' })
const loading = ref(false)
const router = useRouter()
const message = useMessage()
const authStore = useAuthStore()

const rules = {
  username: { required: true, message: '请输入用户名', trigger: 'blur' },
  password: { required: true, message: '请输入密码', trigger: 'blur' }
}

const handleLogin = async () => {
  formRef.value?.validate(async (errors) => {
    if (!errors) {
      try {
        loading.value = true
        await authStore.login(form.value)
        message.success('登录成功')
        router.push('/')
      } catch (error: any) {
        message.error(error.response?.data?.msg || '登录失败')
      } finally {
        loading.value = false
      }
    }
  })
}
</script>

特点

  • 表单校验(Naive UI Form)
  • 加载状态
  • 错误提示友好

第六章:主布局与权限控制

6.1 MainLayout.vue

复制代码
<!-- src/layouts/MainLayout.vue -->
<template>
  <n-layout has-sider>
    <n-layout-sider bordered collapse-mode="width" :collapsed-width="64" :width="240">
      <n-menu v-model:expanded-keys="expandedKeys" v-model:value="activeKey" :options="menuOptions" />
    </n-layout-sider>
    <n-layout>
      <n-layout-header>
        <div class="header">
          <span>MyApp</span>
          <n-button @click="handleLogout">退出</n-button>
        </div>
      </n-layout-header>
      <n-layout-content>
        <router-view />
      </n-layout-content>
    </n-layout>
  </n-layout>
</template>

<script setup lang="ts">
import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth'

const router = useRouter()
const authStore = useAuthStore()

const activeKey = computed({
  get: () => router.currentRoute.value.name as string,
  set: (key) => router.push({ name: key })
})

const expandedKeys = ref<string[]>([])
const menuOptions = [
  { key: 'Dashboard', label: '仪表盘', icon: renderIcon(DashboardOutline) },
  { key: 'Profile', label: '个人资料', icon: renderIcon(PersonOutline) }
]

const handleLogout = () => {
  authStore.logout()
  router.push('/login')
}
</script>

第七章:与 Flask API 对接细节

7.1 后端 CORS 配置

确保 Flask 允许前端域名访问:

复制代码
# app.py
from flask_cors import CORS

def create_app():
    app = Flask(__name__)
    CORS(app, origins=['http://localhost:5173', 'https://your-domain.com'])  # ← 关键
    # ...

安装 flask-cors

复制代码
pip install flask-cors

7.2 Token 刷新接口

Flask 需提供 /auth/refresh 接口:

复制代码
@app.route('/auth/refresh', methods=['POST'])
@jwt_required(refresh=True)
def refresh():
    identity = get_jwt_identity()
    access_token = create_access_token(identity=identity)
    return jsonify(access_token=access_token)

第八章:PWA 与离线支持(可选)

8.1 安装 Vite PWA 插件

复制代码
npm install -D vite-plugin-pwa

配置 vite.config.ts

复制代码
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { VitePWA } from 'vite-plugin-pwa'

export default defineConfig({
  plugins: [
    vue(),
    VitePWA({
      registerType: 'autoUpdate',
      manifest: {
        name: 'MyApp',
        short_name: 'MyApp',
        description: 'A modern web app',
        theme_color: '#ffffff',
        icons: [
          { src: 'icon-192.png', sizes: '192x192', type: 'image/png' },
          { src: 'icon-512.png', sizes: '512x512', type: 'image/png' }
        ]
      }
    })
  ],
  // ...
})

用户可将网站"安装"到桌面,支持离线访问。


第九章:生产构建与部署

9.1 构建前端

复制代码
npm run build

输出到 dist/ 目录。

9.2 Nginx 配置(前后端同域)

更新后端 nginx.conf

复制代码
server {
  listen 443 ssl;
  server_name your-domain.com;

  # 前端静态文件
  location / {
    root /app/frontend/dist;  # Docker 内路径
    try_files $uri $uri/ /index.html;  # SPA 路由支持
  }

  # API 代理
  location /api/ {
    proxy_pass http://web:8000/;  # Flask 服务
    # ... 其他 proxy 设置 ...
  }
}

9.3 Dockerfile 前端构建(多阶段)

更新主 Dockerfile

复制代码
# === 阶段1:构建前端 ===
FROM node:18-alpine AS frontend-builder
WORKDIR /app
COPY frontend/package*.json ./
RUN npm ci
COPY frontend/ .
RUN npm run build

# === 阶段2:构建后端 ===
FROM python:3.11-slim AS backend-builder
# ... 原有后端构建 ...

# === 阶段3:最终镜像 ===
FROM python:3.11-slim
# ... 安装 tzdata, 创建用户 ...

# 复制后端代码与依赖
COPY --from=backend-builder --chown=app:app /home/app/.local /home/app/.local
COPY --chown=app:app . .

# 复制前端构建产物
COPY --from=frontend-builder --chown=app:app /app/dist ./frontend/dist

# ...

优势:单镜像包含前后端,部署简单。


第十章:性能优化与最佳实践

优化项 方案
懒加载路由 () => import('@/views/...')
图片压缩 使用 WebP 格式,<img loading="lazy">
API 缓存 Pinia store + setTimeout 清理
暗色主题 Naive UI 内置,通过 localStorage 切换
错误边界 全局 onErrorCaptured 捕获组件错误
相关推荐
yangminlei2 小时前
Spring Boot 实现 DOCX 转 PDF
开发语言·spring boot·python
万行2 小时前
机器学习&第六.七章决策树,集成学习
人工智能·python·算法·决策树·机器学习·集成学习
weixin_462446232 小时前
Python+React 专为儿童打造的汉字学习平台:从学前到小学的智能汉字教育解决方案
python·学习·react.js
河码匠2 小时前
Django rest framework 自定义url
后端·python·django
cnxy1882 小时前
Python Web开发新时代:FastAPI vs Django性能对比
前端·python·fastapi
神仙姐姐QAQ2 小时前
vue3更改.el-dialog__header样式不生效
前端·javascript·vue.js
脾气有点小暴2 小时前
uniapp真机调试无法连接
前端·uni-app
AI_56782 小时前
Vue.js 深度开发指南:从数据绑定到状态管理的最佳实践
前端·javascript·vue.js
Irene19912 小时前
Sass常用语法总结
前端·sass