Vite项目搭建AIGC图片工作台:生图轮询与动态特效
本项目从Vite空项目出发,构建了AIGC图片创作工作台,集成React前端与Express后端,实现了文生图、任务异步轮询、图片本地持久化、生成库管理,以及Canvas滤镜与动态雨滴特效,支持PNG和WebM格式导出,提供了完整的AIGC图片创作解决方案。
从 Vite 空项目到 AIGC 图片工作台:打通生图、任务轮询、生成库与 Canvas 动态特效
很多 AIGC 项目的第一版,往往只有一个输入框:用户输入 Prompt,前端调用模型接口,页面显示一张图片。这个流程足以验证模型能力,但离真正的应用还有距离。真实项目还要面对异步任务、失败处理、接口密钥安全、图片链接过期、历史记录、文件下载,以及生成后的二次编辑。

最近从一个能正常运行的 Vite 空项目开始,逐步完成了一个名为 AIGC Creative Studio 的图片创作工作台。它目前已经打通了从 React 表单到 Express 创建本地任务,再到调用阿里云百炼万相、查询外部任务状态、下载并持久化图片、前端自动轮询、生成库管理、Canvas 图片编辑,以及 PNG/WebM 导出的完整流程。
这篇文章不只是展示最终效果,更会完整复盘开发的顺序、过程中遇到的问题,以及这个项目对前端和全栈能力有何帮助。
一、为什么要做这个项目
有前端、全栈和 HarmonyOS 开发经历,做过 Web 容器、Ja vaScript Bridge 和异步通信相关项目。在完成跨端运行时项目之后,希望再做一个更贴近当前应用方向的作品。这个项目需要满足几个条件:不是只展示静态页面;有真实的第三方 AI 服务;有前后端通信和异步任务;有文件处理与本地持久化;有能体现前端深度的交互;可以部署、演示,也可以继续扩展。
因此,选择了“AIGC 图片创作工作台”。没有一开始就创建复杂脚手架,也没有直接生成一个庞大的项目。第一步只是:npm create vite@latest,选择 React、TypeScript、ESLint。先让空项目正常启动,再一点点增加功能。这样做的好处是,每一步都知道自己加入了什么,也更容易定位问题。
二、技术栈
当前项目的主要技术栈如下:
前端: Vite、React、TypeScript、React Router、Canvas 2D API、MediaRecorder、原生 Fetch API。
后端: Node.js、Express、TypeScript、原生 Fetch API、本地 JSON 元数据存储、本地文件存储。
AIGC 服务: 阿里云百炼、万相文生图模型、异步任务接口。
没有在第一版引入 UI 组件库、Redux、数据库、Redis 和消息队列。不是因为这些技术没有价值,而是因为 MVP 阶段最重要的是先验证核心链路。
三、第一阶段:先做一个纯前端创作台
第一版页面只有三个区域。顶部导航展示项目名称、页面入口和后端服务状态。左侧参数面板包括 Prompt、Negative Prompt、图片比例、生成数量、Seed、风格预设和开始生成按钮。右侧结果区域根据任务状态展示空状态、提交中、等待处理、生成中、生成成功、生成失败。
表单状态最初直接使用 React useState 管理。这个阶段不接接口,只验证页面结构、响应式布局和交互状态。
这里有一个很实际的经验:结果区域要保持稳定高度。如果空状态、加载状态、失败状态和图片状态的高度完全不同,轮询期间整个页面会不断跳动。按钮文字从“刷新状态”变成“查询中”时,如果没有设置稳定宽度,也会造成明显抖动。因此,后来做了两项调整:结果区域设置稳定的最小高度;状态按钮设置固定宽度,并区分自动轮询状态与手动查询状态。
四、第二阶段:建立最小后端
前端页面稳定后,在项目中增加 server 目录,使用 Express 和 TypeScript 搭建后端。第一个接口不是生图,而是健康检查:GET /api/health,响应:{"success": true, "message": "AIGC Creative Studio API is running"}。然后让前端导航栏显示服务检测中、服务正常、服务未连接。
这个接口看起来简单,但它验证了第一条真正的全栈链路:React → Fetch → Express → JSON → React 状态。
后来增加路由时,还遇到过一个问题:/create 页面显示“服务未连接”,而 /library 页面显示“服务正常”。原因不是后端不稳定,而是两个页面分别维护了一份健康检查状态。最终把 Header 放到公共布局中,只保留一份应用级状态。推荐结构如下:BrowserRouter → AppLayout → Header 和 Routes(CreatePage、LibraryPage、EditorPage)。服务状态属于整个应用,不应该分别散落在页面组件里。
五、生成任务为什么不能只返回一张图片
真实生图通常不是立即完成的。请求提交到模型平台后,平台先返回一个任务 ID,任务状态随后经历 PENDING → RUNNING → SUCCEEDED / FAILED。因此,后端不能把它设计成普通同步接口。
项目中的本地任务状态为:'pending' | 'processing' | 'succeeded' | 'failed'。创建接口:POST /api/generations,查询接口:GET /api/generations/:taskId。前端提交参数后,后端立即创建本地任务,返回 {"success": true, "data": {"taskId": "本地任务 ID", "status": "pending"}},随后由后端调用模型服务,并更新本地任务状态。这样做的好处是,前端只需要理解自己系统的任务协议,不需要直接依赖某一家模型平台的字段。
六、Provider 抽象:不要把业务代码绑死在模型平台上
在接入真实万相接口之前,先定义了 Provider 接口:interface ImageGenerationProvider { readonly name: string; generate(input: GenerateImageInput): Promise。业务层只关心输入生成参数、等待生成结果、得到图片数组。至于底层使用万相、OpenAI Images、本地 Stable Diffusion,还是其他服务,由 Provider 负责。这层抽象解决了两个问题:第三方接口字段不会污染业务路由;以后替换模型时,不需要重写任务和生成库。
当前使用的是 WanxImageProvider,主要负责:读取环境变量、创建万相异步任务、查询任务状态、映射图片比例、处理超时和错误、提取图片 URL、转换为项目内部统一结果。为了避免开发期间意外消耗额度,还增加了安全开关 ENABLE_REAL_GENERATION=false,需要真实生成时才设置为 true。这不是模型平台要求的字段,而是项目自己的成本保护机制。
七、接入阿里云百炼万相
测试阶段选择了成本较低、带有新人免费额度的万相文生图模型。后端环境变量示例:PORT=3001、DASHSCOPE_API_KEY=、DASHSCOPE_MODEL=wanx2.0-t2i-turbo、DASHSCOPE_BASE_URL=https://dashscope.aliyuncs.com/api/v1、ENABLE_REAL_GENERATION=false。真实 .env 不应该提交到 Git。万相旧版文生图使用异步流程:创建任务 → 获得外部 task_id → 定时查询 → 获取图片 URL。Provider 内部将外部状态映射为项目状态:PENDING → pending、RUNNING → processing、SUCCEEDED → succeeded、FAILED → failed。前端则根据本地 taskId 自动轮询:等待处理 → 生成中 → 生成完成。任务进入 succeeded 或 failed 后立即停止轮询。组件卸载、任务变化或重新生成时,也会清理旧定时器。这里需要注意:轮询不是越快越好,频率过高会增加服务器和第三方接口压力,甚至触发限流。
八、失败信息必须真正展示出来
最初失败时,页面只显示“生成失败,请稍后重试”,这对用户和开发者都不够友好。后来任务增加了结构化错误:{"code": "REAL_GENERATION_DISABLED", "message": "Real image generation is disabled", "retryable": false}。前端失败卡片展示:生成失败、错误原因、可选错误码、重新生成按钮。但不能把以下内容返回给用户:API Key、Authorization、服务器绝对路径、完整异常堆栈、第三方敏感响应。好的错误处理,不只是 catch 一下,而是要在安全与可诊断性之间取得平衡。
九、为什么必须把图片下载到本地
模型服务返回的图片通常是临时 URL。如果生成库直接保存这个 URL,过一段时间后,历史图片就会全部失效。因此,Provider 成功后,后端立即执行:读取临时 URL → 下载图片二进制 → 保存到 storage/images → 将任务中的 URL 替换为本地地址。本地地址示例:/api/images/{taskId}-0.png。任务元数据保存到 server/data/generations.json,结构类似:包含 taskId、status、request(prompt、negativePrompt、aspectRatio、count、style)、result(images 数组,其中 url 为本地地址)。服务启动时重新读取 JSON,恢复历史任务。当前阶段使用 JSON 是为了快速完成 MVP,它不适合多实例和高并发写入,后续计划迁移 PostgreSQL;图片则可以从本地目录迁移到 OSS。
十、生成库:让“生成历史”真正有意义
最初导航栏同时出现了“生成库”和“生成历史”,实际功能完全重合。后来删除重复入口,只保留“图片创作 | 生成库 | 服务状态”。生成库接口:GET /api/generations?status=succeeded&limit=20&offset=0。生成库展示:图片、Prompt、风格、图片比例、生成时间、下载、新窗口查看、编辑、删除。图片下载没有直接依赖第三方 URL,而是通过后端下载接口:GET /api/generations/:taskId/images/:imageIndex/download。后端只能读取任务中已有的图片地址,不接受用户提交任意 URL,从而降低 SSRF 风险。删除功能同样需要同时处理两部分:删除本地图片文件 + 更新任务元数据。如果一个任务的最后一张图片被删除,则一并清理任务记录。
十一、Canvas 编辑器:项目真正有辨识度的部分
单纯调用生图 API,更多体现接口集成能力。为了增加前端技术深度,又加入了 Canvas 编辑器。路由:/editor/:taskId/:imageIndex。编辑器会:根据任务 ID 查询任务;找到指定图片;加载本地图片;绘制到原始分辨率的 Canvas;在页面中按容器尺寸缩放显示。页面看到的是缩放后的 Canvas,但导出时仍使用原始分辨率,避免把页面显示尺寸误当成图片输出尺寸。
十二、黑白滤镜与灰度渐变
黑白滤镜不是简单使用 CSS filter: grayscale(1),而是真正处理 Canvas 像素。灰度值计算:gray = 0.299 × red + 0.587 × green + 0.114 × blue。强度混合:result = original × (1 - intensity) + gray × intensity。每次调整都从原始 ImageData 重新计算,不能基于上一次处理结果继续计算,否则来回拖动后会累计失真。在此基础上,又实现了“灰度到彩色”的横向渐变:左侧黑白、右侧彩色、中间平滑过渡,可以拖动分界位置,可以调节过渡宽度。过渡使用 smoothstep:t = clamp((x - start) / (end - start), 0, 1),smooth = t × t × (3 - 2 × t),最后使用 smooth 混合彩色与灰度值。
十三、动态雨滴与“雨滴唤醒色彩”
普通静态雨丝可以通过 Canvas 绘制半透明线段完成,但项目中更有辨识度的效果是:整张图片变灰 → 用户设置雨滴落点 → 雨滴从顶部落下 → 落点出现涟漪 → 涟漪覆盖区域恢复原图色彩。这个效果使用两份离屏画布:colorCanvas(彩色原图)和 grayscaleCanvas(灰度图片)。每一帧:主画布绘制灰度底图;绘制下落雨滴;雨滴到达目标点后进入涟漪阶段;使用圆形裁剪区域绘制彩色图层;绘制逐渐扩散并淡出的涟漪圆环。
伪代码如下:ctx.drawImage(grayscaleCanvas, 0, 0); ctx.sa ve(); ctx.beginPath(); ctx.arc(centerX, centerY, radius, 0, Math.PI * 2); ctx.clip(); ctx.drawImage(colorCanvas, 0, 0); ctx.restore();。动画使用 requestAnimationFrame,并通过 deltaTime 更新位置,避免不同刷新率下动画速度不一致。此外还需要处理:切换工具时取消动画;组件卸载时取消动画;页面不可见时暂停;React StrictMode 下避免双动画循环;动画数据放在 useRef,不在每一帧触发 React 渲染。
十四、PNG 与 WebM 导出
Canvas 当前画面可以通过 canvas.toBlob() 导出 PNG。但 PNG 只能保存静态瞬间。为了保存完整的雨滴与涟漪动画,项目又使用 canvas.captureStream(30) 配合浏览器原生 MediaRecorder 导出 WebM。大致流程:重置动画 → captureStream 获取 Canvas 视频流 → MediaRecorder 开始录制 → 播放雨滴与涟漪动画 → 动画完成后保留最终画面 → 停止录制 → 合并 Blob → 下载 WebM。格式选择需要逐级检测:video/webm;codecs=vp9、video/webm;codecs=vp8、video/webm。不能默认所有浏览器都支持 VP9。录制结束后,还必须清理:MediaStream Track、MediaRecorder、Blob URL、超时定时器、requestAnimationFrame。
十五、编辑后的图片如何回到生成库
Canvas 编辑完成后,除了本地下载,还可以保存回生成库。前端通过 canvas.toBlob() 得到 PNG,再发送:POST /api/generations/:taskId/images/:imageIndex/edits,Content-Type: image/png。后端使用路由级 express.raw() 接收二进制,校验:Content-Type、文件大小、PNG 文件头、原任务、原图片索引、存储路径。编辑后的图片不会覆盖原图,而是作为新资产追加:AI 生成图 → 编辑作品。生成库中可以继续查看、下载和再次编辑。至此,项目形成完整闭环:生成 → 保存 → 浏览 → 编辑 → 导出 → 再保存 → 删除。
十六、开发中遇到的几个典型问题
1. .env 明明存在,却读取不到。 曾经执行 node -e "require('dotenv').config(); console.log(process.env.ENABLE_REAL_GENERATION)",结果是 undefined。最终发现配置没有正确写入真实的 server/.env,或者文件名实际是 .env.txt。排查环境变量时,不要直接打印 API Key,可以只判断是否存在:Boolean(process.env.DASHSCOPE_API_KEY)。
2. 健康检查正常,生图却提示无法连接。 增加 React Router 后,如果请求写成 fetch('api/generations'),在 /create 页面可能被解析为 /create/api/generations。因此统一封装了 API Base URL:VITE_API_BASE_URL=http://localhost:3001,所有健康检查、任务创建、轮询、图片地址和下载地址都通过同一个方法拼接。
3. 图片能在 Network 看到,却不能在页面下载。 跨域图片使用 时,浏览器不一定直接下载。因此增加后端下载袋里,只下载任务中已经保存的本地图片,再设置 Content-Disposition: attachment。
4. Canvas 跨域污染。 如果图片跨域加载且响应头不正确,调用 toBlob() 或 getImageData() 时可能出现 SecurityError: canvas has been tainted。因此图片加载需要正确的 CORS 配置,并在设置 src 之前配置 image.crossOrigin = 'anonymous'。
十七、如何运行项目
克隆:git clone https://github.com/lichenyang5/AIGC-Creative-Studio.git,cd AIGC-Creative-Studio。安装前端依赖:npm install。安装后端依赖:cd server; npm install。根据 .env.example 创建 server/.env。示例:PORT=3001、DASHSCOPE_API_KEY=你的APIKey、DASHSCOPE_MODEL=wanx2.0-t2i-turbo、DASHSCOPE_BASE_URL=https://dashscope.aliyuncs.com/api/v1、ENABLE_REAL_GENERATION=true。不要将真实 .env 提交到 GitHub。启动后端:cd server; npm run dev。启动前端:npm run dev。打开:http://localhost:5173。如果只想调试前后端链路、不调用真实模型,设置 ENABLE_REAL_GENERATION=false。
十八、为什么暂时没有数据库
项目目前使用:任务数据(generations.json)、图片文件(storage/images)。原本计划使用 PostgreSQL + Prisma + Docker,但在受限网络环境中,Docker 无法正常拉取 PostgreSQL 镜像。因此没有为了“看起来技术栈更多”而强行改造,而是先保留可运行的 JSON Repository。后续网络条件允许时,计划迁移:JSON → PostgreSQL,本地图片 → OSS。数据库只保存任务和图片元数据,不直接存储大图片二进制。这个取舍也让人更加明确:项目开发不是技术名词收集,而是根据当前目标、环境和成本做选择。
十九、这个项目体现了哪些能力
从求职角度看,这个项目的价值不只在“AIGC”三个字。
前端能力: React 组件拆分、TypeScript 类型设计、React Router 公共布局、表单和异步状态管理、自动轮询和生命周期清理、响应式工作台、Canvas 像素处理、Pointer Events、requestAnimationFrame、MediaRecorder、文件导出和下载、错误、空状态和加载状态设计。
后端能力: Express 接口设计、参数校验、异步任务建模、Provider 抽象、第三方 API 集成、错误码转换、文件下载和持久化、二进制上传、静态资源服务、路径安全和 SSRF 防护、JSON Repository。
工程能力: 环境变量管理、API Key 安全、成本开关、前后端统一协议、本地任务与第三方任务解耦、渐进式开发、Git 阶段提交、为数据库和对象存储预留迁移空间。
二十、下一步计划
项目核心功能已经打通,后续重点不会继续无限堆滤镜,而是提高工程完整度:使用 Vitest + Supertest 增加后端接口测试;增加前端核心组件测试;完善 README、架构图和截图;增加演示视频;在合适环境下迁移 PostgreSQL;将图片存储迁移到 OSS;增加用户和权限体系;部署前端和后端;对生成频率和成本增加限流。
结语
这个项目最重要的收获,不是成功调用了一次生图 API。真正有价值的是把一个模型调用,逐步变成一个完整的软件流程:输入 → 校验 → 创建任务 → 异步处理 → 状态同步 → 文件持久化 → 历史管理 → 图片编辑 → 动态导出。如果只看最终页面,很多功能似乎理所当然。但把它们逐个实现后,会发现 AIGC 应用本质上仍然离不开传统的软件工程能力。模型负责生成内容,应用负责让这项能力真正可用。
如果也准备做一个 AIGC 项目,建议是:不要第一天就设计数据库、队列、登录和复杂架构。先从一个可运行的页面开始,完成最小链路,然后每次只解决一个真实问题。当每一步都能运行、验证和提交时,最后得到的不只是一个 Demo,而是一个自己真正理解的项目。
参考资料
- 项目源码:AIGC-Creative-Studio
- 阿里云百炼万相文生图 API:万相文生图 V2 API 参考
- 阿里云百炼模型价格:模型调用价格
游乐网为非赢利性网站,所展示的游戏/软件/文章内容均来自于互联网或第三方用户上传分享,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系youleyoucom@outlook.com。
同类文章
CAD零基础入门教程:坐标输入、图层管理与基础绘图命令
本文面向CAD零基础学习者,系统讲解坐标输入、图层管理与基础绘图命令的核心用法。通过分步实操与常见问题排查,帮助新手建立精确绘图习惯,掌握规范出图的基础能力。
CAD从入门到项目交付:绘图、标注、图块与实战工作流
掌握CAD的核心在于建立“画得准、标得清、复用快、交付稳”的工作流。本文提供从环境设置、高频命令组合、标注规范、图块标准化到项目分阶段交付的完整路径,帮助初学者避免常见返工陷阱,独立完成可检查、可复用、可打印的工程图纸。
Claude Code 登录指南:个人、Teams 与企业账号区分与授权步骤
本文详细解析 Claude Code 登录前的账号类型区分方法,涵盖个人订阅、Teams 席位与企业 Enterprise 席位的授权路径差异。提供终端登录命令、环境变量排查及常见异常处理步骤,帮助用户快速完成正确授权并避免登录路径混淆。
Claude Code 文件修改前的权限模式配置与命令审批指南
本文详细介绍Claude Code在修改文件前的权限模式配置方法,包括defaultMode可选值、permissions allow与deny规则设置、多层级配置文件管理以及 status验证技巧,帮助开发者安全高效地使用AI编程助手。
Claude Code接入VS Code后先测扩展和终端命令
在VS Code中接入Claude Code后,建议优先验证扩展面板与集成终端两条入口。本文提供标准检查顺序、关键命令与常见故障排查路径,帮助你快速确认环境就绪,避免后续开发受阻。
- 热门数据榜
1
2
3
4
5
6
7
8
9
10
相关攻略
2026-09-01 16:53
2026-09-01 16:52
2026-09-01 14:27
2026-09-01 14:12
2026-09-01 14:10
2026-09-01 14:07
2026-09-01 13:55
2026-09-01 13:47
热门教程
- 游戏攻略
- 安卓教程
- 苹果教程
- 电脑教程

