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 捕获组件错误
相关推荐
用户69371750013843 小时前
Google 正在“收紧侧加载”:陌生 APK 安装或需等待 24 小时
android·前端
蓝帆傲亦3 小时前
Web 前端搜索文字高亮实现方法汇总
前端
用户69371750013843 小时前
Room 3.0:这次不是升级,是重来
android·前端·google
qq_417695055 小时前
机器学习与人工智能
jvm·数据库·python
漫随流水5 小时前
旅游推荐系统(view.py)
前端·数据库·python·旅游
yy我不解释6 小时前
关于comfyui的mmaudio音频生成插件时时间不一致问题(一)
python·ai作画·音视频·comfyui
踩着两条虫6 小时前
VTJ.PRO 核心架构全公开!从设计稿到代码,揭秘AI智能体如何“听懂人话”
前端·vue.js·ai编程
紫丁香7 小时前
AutoGen详解一
后端·python·flask
FreakStudio7 小时前
不用费劲编译ulab了!纯Mpy矩阵micronumpy库,单片机直接跑
python·嵌入式·边缘计算·电子diy
jzlhll1237 小时前
kotlin Flow first() last()总结
开发语言·前端·kotlin