ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Vite工程化前端集成Qwen Image多模态生图模型实战指南

Vite工程化前端集成Qwen Image多模态生图模型实战指南

1. 项目概述:当工程化前端遇上多模态生图

最近在折腾一个挺有意思的项目,核心目标是把阿里通义千问的Qwen Image多模态图像生成模型,集成到一个现代化的Vite前端工程里。听起来像是把两个不同次元的东西硬凑在一起?一开始我也这么觉得。但实际做下来,发现这背后其实是一个典型的“前端智能化”场景:我们不再满足于静态的UI和交互,而是希望前端应用能直接调用强大的AI能力,实时生成内容,创造更动态、更个性化的用户体验。比如,一个在线设计工具,用户输入文字描述,侧边栏实时渲染出风格草图;或者一个社区应用,用户发帖时能即时生成配图。Qwen Image作为一款表现不错的文生图模型,提供了这样的能力,而Vite则代表了当下最主流、高效的前端开发与构建范式。这个项目的挑战和乐趣,就在于如何用前端工程师熟悉的工具链和思维方式,去安全、高效地“桥接”并调用这个深度学习模型,让AI能力真正落地到浏览器端或通过前端可控的后端服务触手可及。

2. 核心架构设计与技术选型考量

2.1 为什么是Vite + Qwen Image的组合?

这个组合乍看有点跨界,但细想之下逻辑很清晰。Vite解决的是前端开发的体验和效率问题:极速的服务启动、闪电般的HMR(热模块替换)、以及面向现代浏览器的优化构建。它让我们能快速搭建一个响应式、模块化的前端应用界面,用于收集用户输入(文本提示词、风格参数等)和展示生成的图像。而Qwen Image代表的是多模态AI的图像生成能力。前端直接调用庞大的深度学习模型是不现实的,因此架构的关键在于“桥接”。通常有两种模式:一种是前端直接调用模型提供的API(如果模型服务已部署在云端);另一种是前端与一个自建的、封装了模型推理的后端服务通信。本项目更侧重于后者,即探讨如何在一个由Vite驱动的工程化前端项目中,设计并实现与Qwen Image模型服务的通信链路。

技术栈的深层考量:

  1. 开发效率与体验:Vite的Dev Server基于原生ESM,避免了传统打包器在开发时的打包开销,这使得我们能在修改前端界面和业务逻辑时获得即时反馈,对于需要频繁调整生图参数和UI交互的项目来说至关重要。
  2. 构建优化:Vite在生产构建时使用Rollup,能对代码进行高效的Tree Shaking和资源优化,确保最终交付给用户的资源体积最小化。虽然模型推理在后端,但前端可能涉及图片预览、历史记录管理等,优化的资源加载能提升用户体验。
  3. 现代前端生态:Vite与Vue 3、React、Svelte等现代框架无缝集成,便于我们使用这些框架丰富的状态管理(如Pinia、Zustand)和UI组件库来构建复杂的交互界面,例如提示词输入框、生图参数面板、画廊视图等。
  4. AI能力集成:Qwen Image模型通常需要部署在具有GPU计算能力的服务器上。前端通过HTTP(如RESTful API)或WebSocket与后端服务通信。这里的关键是设计一套清晰、类型安全的接口契约,并处理好异步请求、错误处理、长时任务轮询(因为生图可能耗时数秒到数十秒)以及安全认证(如API Key管理)。

2.2 整体架构蓝图

一个可行的架构分为三层:

  • 前端展示层 (Vite Project):负责用户交互界面。提供提示词输入、参数(如尺寸、风格、数量)配置、生成按钮、加载状态展示、生成结果画廊、历史记录查看等功能。使用Axios或Fetch API与后端服务通信。
  • 后端代理/服务层 (Node.js/ Python Service):这是关键桥梁。它接收前端的请求,进行必要的验证、参数预处理,然后调用Qwen Image模型的推理接口。模型可以部署在本地服务器(需GPU),或调用云服务商提供的API(如阿里云灵积平台)。这一层还负责处理敏感信息(如API密钥)、限流、日志记录,并将模型返回的图片(通常是Base64编码或URL)转发给前端。
  • 模型推理层 (Qwen Image):实际执行文本到图像生成的深度学习模型。它运行在具备足够算力的环境中。

对于前端开发者而言,重点在于第一层和与第二层的交互设计。我们需要在Vite项目中,工程化地组织API调用代码、状态管理和UI组件。

3. Vite工程化环境搭建与核心配置

3.1 初始化项目与基础依赖

首先,我们使用Vite官方脚手架快速初始化一个项目。这里以React + TypeScript为例,因为类型安全在对接API时能减少很多低级错误。

npm create vite@latest qwen-image-frontend -- --template react-ts cd qwen-image-frontend npm install

接下来,安装项目必需的依赖:

  • axios: 用于发起HTTP请求,比原生fetch功能更完善,拦截器、请求取消等特性很实用。
  • zustand@reduxjs/toolkit: 用于状态管理。生图应用涉及加载状态、生成结果列表、用户设置等全局状态,一个好的状态管理库是必须的。Zustand更轻量,Redux Toolkit更体系化,根据团队习惯选择。
  • tailwindcssantd/mui: UI样式框架。快速构建美观的界面。这里选择Tailwind CSS进行演示,因其灵活性高。
npm install axios zustand npm install -D tailwindcss postcss autoprefixer npx tailwindcss init -p

配置tailwind.config.js和全局CSS,这些是标准步骤,不再赘述。

3.2 API服务模块的工程化封装

这是前端项目的核心模块之一。我们不能在组件里随处写axios.post(‘/api/generate‘, ...),而应该进行集中、统一的封装。

第一步:创建API客户端实例 (src/lib/api-client.ts)

import axios from ‘axios‘; // 创建axios实例,配置基础URL和超时时间 const apiClient = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL || ‘http://localhost:3000/api‘, // 从环境变量读取 timeout: 30000, // 生图是长任务,超时时间设长一些,比如30秒 headers: { ‘Content-Type‘: ‘application/json‘, }, }); // 请求拦截器:可用于添加认证Token等 apiClient.interceptors.request.use( (config) => { const token = localStorage.getItem(‘auth_token‘); // 示例 if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }, (error) => Promise.reject(error) ); // 响应拦截器:统一处理错误 apiClient.interceptors.response.use( (response) => response.data, // 直接返回data,简化调用处代码 (error) => { console.error(‘API请求错误:‘, error); // 根据HTTP状态码或后端返回的code进行统一错误提示 const message = error.response?.data?.message || error.message || ‘请求失败‘; // 可以在这里触发一个全局的错误通知 return Promise.reject(new Error(message)); } ); export default apiClient;

第二步:定义类型契约和API函数 (src/services/imageGenerationService.ts)

import apiClient from ‘../lib/api-client‘; // 定义请求参数类型 export interface GenerateImageParams { prompt: string; // 文本提示词 negative_prompt?: string; // 负面提示词(不希望出现的元素) width?: number; // 图像宽度 height?: number; // 图像高度 num_images?: number; // 生成数量 seed?: number; // 随机种子,用于复现结果 // 其他Qwen Image支持的参数... } // 定义响应数据类型 export interface GeneratedImage { id: string; url: string; // 图片的URL(后端返回的可能是托管后的URL或Data URL) base64?: string; // 或者直接是base64编码的图片数据 prompt: string; timestamp: number; } // 具体的API调用函数 export const imageGenerationService = { async generateImages(params: GenerateImageParams): Promise<GeneratedImage[]> { // 这里调用的是我们后端代理服务的端点,而不是直接调用模型 const response = await apiClient.post<{ images: GeneratedImage[] }>(‘/generate‘, params); return response.images; }, async getGenerationHistory(): Promise<GeneratedImage[]> { const response = await apiClient.get<{ history: GeneratedImage[] }>(‘/history‘); return response.history; }, // 其他相关函数... };

注意VITE_API_BASE_URL这样的环境变量需要你在项目根目录的.env.development.env.production文件中定义。这很好地隔离了开发和生产环境的后端地址。

3.3 状态管理设计:管理生图状态与结果

使用Zustand创建一个Store来集中管理所有与生图相关的状态。

// src/stores/useImageStore.ts import { create } from ‘zustand‘; import { GeneratedImage } from ‘../services/imageGenerationService‘; interface ImageState { // 状态 isGenerating: boolean; generatedImages: GeneratedImage[]; historyImages: GeneratedImage[]; error: string | null; // 操作 generateImage: (prompt: string, params?: Omit<GenerateImageParams, ‘prompt‘>) => Promise<void>; clearError: () => void; loadHistory: () => Promise<void>; } export const useImageStore = create<ImageState>((set, get) => ({ isGenerating: false, generatedImages: [], historyImages: [], error: null, generateImage: async (prompt, extraParams) => { if (get().isGenerating) return; // 防止重复提交 set({ isGenerating: true, error: null }); try { const params = { prompt, ...extraParams }; const images = await imageGenerationService.generateImages(params); // 将新生成的图片添加到列表和历史中 set((state) => ({ generatedImages: [...images, ...state.generatedImages], // 新图放前面 historyImages: [...images, ...state.historyImages], isGenerating: false, })); } catch (err: any) { set({ error: err.message, isGenerating: false }); console.error(‘生成图片失败:‘, err); } }, clearError: () => set({ error: null }), loadHistory: async () => { try { const history = await imageGenerationService.getGenerationHistory(); set({ historyImages: history }); } catch (err: any) { set({ error: ‘加载历史记录失败‘ }); } }, }));

这样,在任何React组件中,我们都可以通过useImageStore这个Hook来访问和操作生图状态,实现了逻辑与UI的分离。

4. 前端界面实现与核心交互逻辑

4.1 构建生图控制面板组件

创建一个主要的控制组件,包含提示词输入框、参数设置和生成按钮。

// src/components/ImageGeneratorPanel.tsx import React, { useState } from ‘react‘; import { useImageStore } from ‘../stores/useImageStore‘; const ImageGeneratorPanel: React.FC = () => { const [prompt, setPrompt] = useState(‘‘); const [negativePrompt, setNegativePrompt] = useState(‘‘); const [width, setWidth] = useState(512); const [height, setHeight] = useState(512); const [numImages, setNumImages] = useState(1); const { isGenerating, error, generateImage, clearError } = useImageStore(); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!prompt.trim()) { alert(‘请输入提示词‘); return; } await generateImage(prompt, { negative_prompt: negativePrompt, width, height, num_images: numImages, }); // 生成后可以不清空提示词,方便微调 }; return ( <div className="p-6 bg-white rounded-xl shadow-lg max-w-2xl mx-auto"> <h2 className="text-2xl font-bold mb-4">Qwen Image 文生图</h2> {error && ( <div className="mb-4 p-3 bg-red-50 text-red-700 rounded-md flex justify-between items-center"> <span>{error}</span> <button onClick={clearError} className="text-red-500 hover:text-red-700">×</button> </div> )} <form onSubmit={handleSubmit}> <div className="mb-4"> <label className="block text-sm font-medium mb-1">正向提示词 *</label> <textarea className="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent" rows={3} placeholder="描述你想要的画面,例如:一只戴着礼帽的柯基犬,在巴黎街头喝咖啡,电影质感..." value={prompt} onChange={(e) => setPrompt(e.target.value)} disabled={isGenerating} /> </div> <div className="mb-4"> <label className="block text-sm font-medium mb-1">负面提示词(可选)</label> <input type="text" className="w-full p-3 border border-gray-300 rounded-md" placeholder="不希望出现的元素,如:模糊,丑陋,多只手..." value={negativePrompt} onChange={(e) => setNegativePrompt(e.target.value)} disabled={isGenerating} /> </div> <div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6"> <div> <label className="block text-sm font-medium mb-1">宽度</label> <select className="w-full p-2 border rounded" value={width} onChange={(e)=>setWidth(Number(e.target.value))} disabled={isGenerating}> <option value="256">256</option> <option value="512">512</option> <option value="768">768</option> <option value="1024">1024</option> </select> </div> {/* 高度、数量选择器类似 */} </div> <button type="submit" disabled={isGenerating} className={`w-full py-3 px-4 rounded-md font-semibold ${isGenerating ? ‘bg-blue-400 cursor-not-allowed‘ : ‘bg-blue-600 hover:bg-blue-700‘} text-white transition`} > {isGenerating ? ( <span className="flex items-center justify-center"> <svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle> <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> </svg> 生成中... </span> ) : ( ‘开始生成‘ )} </button> </form> </div> ); }; export default ImageGeneratorPanel;

4.2 实现图片画廊与加载状态展示

另一个核心组件是展示生成结果的画廊。我们需要处理图片加载状态、展示大图等。

// src/components/ImageGallery.tsx import React from ‘react‘; import { useImageStore } from ‘../stores/useImageStore‘; const ImageGallery: React.FC = () => { const { generatedImages, isGenerating } = useImageStore(); const [selectedImage, setSelectedImage] = useState<GeneratedImage | null>(null); if (generatedImages.length === 0 && !isGenerating) { return <div className="text-center py-12 text-gray-500">暂无生成的图片,请输入提示词开始创作。</div>; } return ( <div className="mt-8"> <h3 className="text-xl font-semibold mb-4">生成结果</h3> {isGenerating && ( <div className="mb-4 p-4 border border-blue-200 bg-blue-50 rounded-md text-center"> <p>AI正在努力绘制中,请稍候...</p> {/* 可以放置一个骨架屏 */} <div className="grid grid-cols-2 md:grid-cols-3 gap-4 mt-4"> {[...Array(3)].map((_, i) => ( <div key={i} className="bg-gray-200 animate-pulse h-48 rounded-md"></div> ))} </div> </div> )} <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4"> {generatedImages.map((img) => ( <div key={img.id} className="border rounded-lg overflow-hidden shadow-sm hover:shadow-md transition-shadow cursor-pointer" onClick={() => setSelectedImage(img)} > <img src={img.url || `data:image/png;base64,${img.base64}`} alt={`Generated: ${img.prompt}`} className="w-full h-48 object-cover" loading="lazy" onError={(e) => { (e.target as HTMLImageElement).src = ‘/fallback-image.png‘; }} // 加载失败处理 /> <div className="p-3"> <p className="text-sm text-gray-600 truncate" title={img.prompt}>{img.prompt}</p> </div> </div> ))} </div> {/* 图片预览模态框 */} {selectedImage && ( <div className="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-50 p-4" onClick={() => setSelectedImage(null)}> <div className="bg-white rounded-lg max-w-4xl max-h-[90vh] overflow-auto" onClick={(e) => e.stopPropagation()}> <img src={selectedImage.url || `data:image/png;base64,${selectedImage.base64}`} alt="预览" className="w-full h-auto" /> <div className="p-4"> <p className="font-medium">提示词:</p> <p className="text-gray-700 mt-1">{selectedImage.prompt}</p> <button className="mt-4 px-4 py-2 bg-gray-200 rounded hover:bg-gray-300" onClick={() => setSelectedImage(null)}>关闭</button> </div> </div> </div> )} </div> ); };

4.3 集成与路由配置

最后,在App.tsx中将所有组件组合起来,并可能添加路由(如果功能复杂,比如有独立的历史页面)。

// src/App.tsx import React, { useEffect } from ‘react‘; import ImageGeneratorPanel from ‘./components/ImageGeneratorPanel‘; import ImageGallery from ‘./components/ImageGallery‘; import { useImageStore } from ‘./stores/useImageStore‘; function App() { const loadHistory = useImageStore((state) => state.loadHistory); useEffect(() => { // 应用启动时加载历史记录 loadHistory(); }, [loadHistory]); return ( <div className="min-h-screen bg-gray-50 p-4 md:p-8"> <header className="mb-8 text-center"> <h1 className="text-4xl font-bold text-gray-800">多模态生图工作台</h1> <p className="text-gray-600 mt-2">基于 Vite + React + Qwen Image 构建</p> </header> <main> <ImageGeneratorPanel /> <ImageGallery /> </main> <footer className="mt-12 text-center text-sm text-gray-500"> <p>提示:生成图片需要时间,请耐心等待。图片质量与提示词描述细节密切相关。</p> </footer> </div> ); } export default App;

5. 前后端联调与核心API对接实战

前端界面完成后,最关键的一步是与后端服务联调。假设我们已经有一个运行在http://localhost:3000的Node.js后端,它提供了/api/generate接口。

5.1 后端接口契约示例

后端接口的响应格式应该与我们前端定义的GeneratedImage类型匹配。一个简单的Node.js + Express示例如下:

// 后端 server.js (简化示例) const express = require(‘express‘); const cors = require(‘cors‘); const { generateImage } = require(‘./qwen-service‘); // 假设的Qwen Image调用封装 const app = express(); app.use(cors()); app.use(express.json()); const history = []; // 简单内存存储,生产环境需用数据库 app.post(‘/api/generate‘, async (req, res) => { try { const { prompt, negative_prompt, width = 512, height = 512, num_images = 1 } = req.body; console.log(`收到生图请求: ${prompt}`); // 1. 调用真正的Qwen Image服务(可能是本地模型或云端API) const imageResults = await generateImage({ prompt, negative_prompt, width, height, num_images, }); // 2. 将结果转换为前端需要的格式 const generatedImages = imageResults.map((imgData, index) => ({ id: `img_${Date.now()}_${index}`, // 假设服务返回的是base64,我们直接传回。或者后端上传到OSS返回URL。 base64: imgData, // 或者 url: `https://your-oss.com/${key}.png` prompt, timestamp: Date.now(), })); // 3. 存入历史 history.unshift(...generatedImages); // 4. 返回给前端 res.json({ images: generatedImages }); } catch (error) { console.error(‘生图服务错误:‘, error); res.status(500).json({ message: error.message || ‘生成失败‘ }); } }); app.get(‘/api/history‘, (req, res) => { res.json({ history: history.slice(0, 50) }); // 返回最近50条 }); app.listen(3000, () => console.log(‘后端代理服务运行在 http://localhost:3000‘));

5.2 前端环境变量与代理配置

在开发环境下,前端运行在http://localhost:5173,后端在http://localhost:3000,存在跨域问题。Vite提供了两种解决方案:

方案一:使用环境变量(推荐)在项目根目录创建.env.development文件:

VITE_API_BASE_URL=http://localhost:3000/api

这样,我们的apiClient就会自动指向这个地址。

方案二:配置Vite开发服务器代理vite.config.ts中配置,可以将前端对/api的请求代理到后端服务,避免跨域。

// vite.config.ts import { defineConfig } from ‘vite‘ import react from ‘@vitejs/plugin-react‘ export default defineConfig({ plugins: [react()], server: { proxy: { ‘/api‘: { target: ‘http://localhost:3000‘, changeOrigin: true, // rewrite: (path) => path.replace(/^\/api/, ‘‘), // 如果后端没有/api前缀,可以重写 }, }, }, })

配置后,前端代码中请求/api/generate就会被Vite Dev Server代理到http://localhost:3000/api/generate,此时apiClientbaseURL可以设为空或‘‘

5.3 联调关键点与技巧

  1. 网络请求查看:打开浏览器开发者工具的“网络”(Network)标签页,查看/api/generate请求的请求体和响应体,确保数据格式符合预期。
  2. 错误处理:在后端接口返回错误时(如HTTP 500),前端拦截器应能捕获并展示友好的错误信息。可以约定一个标准的错误响应格式,如{ code: number, message: string }
  3. 长时任务处理:生图可能超过10秒。要确保前端设置的timeout足够长,并且UI上有明确的加载状态(如禁用按钮、显示加载动画)。更优的方案是后端采用异步任务,先返回一个任务ID,前端轮询或通过WebSocket获取结果。
  4. 图片渲染:如果后端返回的是Base64字符串,前端可以直接用``渲染。但Base64数据量很大,频繁传输和渲染可能影响性能。对于生产环境,最佳实践是后端将生成的图片上传到对象存储(如阿里云OSS、AWS S3)或CDN,然后返回一个短暂的URL给前端。

6. 性能优化与生产环境部署要点

6.1 前端性能优化

  1. 图片懒加载与虚拟滚动:当生成的历史图片很多时,一次性渲染所有``会导致页面卡顿。可以使用loading=“lazy“属性,或引入react-windowreact-virtualized实现虚拟滚动,只渲染可视区域内的图片。
  2. 状态持久化:使用zustand-persist中间件或localForage将用户的历史记录、偏好设置保存在浏览器的IndexedDB中,提升二次访问体验。
  3. 请求防抖与取消:在提示词输入框实时预览的场景下(如果支持),需要对generateImage请求进行防抖。使用axios的CancelToken或AbortController可以取消正在进行的请求,避免无效请求占用资源。
  4. 构建优化:利用Vite的代码分割,将ImageGallery等较重组件动态导入(React.lazy),减少首屏包体积。

6.2 生产环境部署

  1. 环境变量:创建.env.production文件,设置生产环境的API地址。
    VITE_API_BASE_URL=https://your-production-api.com/api
  2. 构建命令:运行npm run build,Vite会在dist目录生成优化后的静态文件。
  3. 静态文件托管:将dist目录的内容部署到任何静态文件服务器,如Nginx、Vercel、Netlify、阿里云OSS等。
  4. CORS配置:确保你的后端生产服务器正确配置了CORS,允许你的前端域名访问。
  5. HTTPS:务必使用HTTPS,特别是涉及任何用户凭证或敏感操作时。

6.3 安全考量

  1. API密钥保护:绝对不要在前端代码中硬编码或暴露访问Qwen Image服务的API密钥。所有密钥都应保存在后端,由后端服务去调用模型API。
  2. 输入验证与清理:后端应对前端传来的prompt等参数进行验证和必要的清理,防止注入攻击。虽然提示词攻击相对特殊,但基本的长度限制、敏感词过滤是必要的。
  3. 限流与防滥用:在后端接口实施限流(如使用express-rate-limit),防止同一个用户或IP地址过度调用,产生高昂的计算成本。

7. 常见问题排查与调试心得

在实际开发中,你肯定会遇到各种问题。下面是一些典型问题及其解决思路:

问题现象可能原因排查步骤与解决方案
前端点击生成后无反应,控制台无报错1. 表单提交阻止默认事件失败。
2. 按钮disabled状态逻辑有误。
3. Store中的isGenerating状态未正确触发。
1. 检查handleSubmit函数是否调用了e.preventDefault()
2. 在generateImage函数开始处添加console.log,确认函数被调用。
3. 使用React DevTools检查组件的state和props变化。
控制台报跨域(CORS)错误前端与后端域名/端口不同,且后端未正确设置CORS头。1. 确认后端服务已启用并运行在指定端口。
2. 在后端代码中确保使用了cors中间件,或手动设置了Access-Control-Allow-Origin等响应头。
3. 开发环境下优先使用Vite的server.proxy配置代理。
请求长时间挂起后报超时错误1. 后端生图任务耗时过长,超过前端axios设置的timeout
2. 后端服务处理请求时卡死或出错。
1. 适当增加timeout值(如60000毫秒)。
2. 查看后端服务日志,确认生图进程是否正常启动和结束。
3. 考虑将接口改为异步模式:立即返回任务ID,前端轮询另一个接口查询结果。
图片无法显示(控制台报404或解码错误)1. 后端返回的图片URL或Base64数据格式不正确。
2. Base64数据不完整或包含非法字符。
3. 图片URL对应的资源不存在或无权访问。
1. 在浏览器网络面板检查API响应,查看images[0].urlbase64字段的值。
2. 如果是Base64,确保前端拼接了正确的Data URL前缀:data:image/png;base64,${base64String}
3. 如果是URL,直接在浏览器地址栏访问该URL,看是否能打开。
生产环境构建后,页面空白或资源加载失败1. 资源路径错误(特别是使用了相对路径)。
2. 环境变量在构建时未被正确注入。
1. 检查Vite配置中是否有base选项,需与部署站点的子路径匹配。
2. 确保生产环境变量以VITE_开头,并在构建前已设置。可以在构建后检查dist目录下生成的HTML文件,看src路径是否正确。
3. 部署到服务器后,检查Nginx等服务器配置是否正确指向了dist目录,并配置了对于SPA的单页应用回退规则(try_files)。

个人踩坑心得

  • 状态管理要趁早:即使项目初期状态简单,也建议尽早引入Zustand或类似库。当需要跨组件共享加载状态、错误信息、用户设置时,你会感谢这个决定。
  • 类型定义是盟友:在TypeScript中严格定义API的请求/响应类型。这不仅能减少运行时错误,还能在联调时快速发现前后端数据格式不一致的问题。
  • 错误反馈要友好:不要只把console.error留给开发者。用户需要知道“为什么失败了”。将后端返回的具体错误信息(如“提示词包含敏感内容”、“GPU资源不足”)经过安全过滤后展示给用户。
  • 长任务用户体验:对于生图这种耗时操作,除了加载动画,还可以考虑加入“预计等待时间”提示(如果后端能提供)、或允许用户同时提交多个任务并在后台运行。
返回列表