<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>前端 on 姚玉亮的网络日志</title><link>https://yusanwen-code.github.io/tags/%E5%89%8D%E7%AB%AF/</link><description>Recent content in 前端 on 姚玉亮的网络日志</description><generator>Hugo -- gohugo.io</generator><language>zh-CN</language><lastBuildDate>Thu, 18 Jun 2026 10:30:00 +0800</lastBuildDate><atom:link href="https://yusanwen-code.github.io/tags/%E5%89%8D%E7%AB%AF/index.xml" rel="self" type="application/rss+xml"/><item><title>独立完成前端页面：Vibe Coding 下的全栈闭环</title><link>https://yusanwen-code.github.io/posts/post-66/</link><pubDate>Thu, 18 Jun 2026 10:30:00 +0800</pubDate><guid>https://yusanwen-code.github.io/posts/post-66/</guid><description>&lt;img src="https://yusanwen-code.github.io/images/post-66-cover.jpg" alt="Featured image of post 独立完成前端页面：Vibe Coding 下的全栈闭环" /&gt;&lt;h2 id="排期等两周不如自己上"&gt;排期等两周，不如自己上&#10;&lt;/h2&gt;&lt;p&gt;我是后端出身，Go 和 Python 写得顺手，前端一直停留在&amp;quot;能改 Vue 模板、写点 jQuery&amp;quot;的水平。&lt;/p&gt;&#10;&lt;p&gt;alchemy-furnace 立项时，我想做一个多人格融合 Agent 的演示平台，需要一个能配置技能包、展示融合血统、对比生成结果的界面。前端同学的排期要等两周，我决定自己上，用 Claude Code 和 Cursor 做 Vibe Coding。&lt;/p&gt;&#10;&lt;p&gt;三周后，我一个人交付了 Go 网关 + Python 合成引擎 + Next.js 前端的完整 DEMO，前端包括技能包编辑器、融合过程可视化、多模型结果对比三个主页面。&lt;/p&gt;&#10;&lt;h2 id="分三步走不让-ai-一把梭"&gt;分三步走，不让 AI 一把梭&#10;&lt;/h2&gt;&lt;p&gt;第一步，先把后端契约定死。用 Go（Gin + GORM）把 API 写好，Swagger 文档直接生成。前端只需要对着文档调接口，不让 AI 猜后端长什么样。这是后端工程师做全栈的最大优势：你能自己定义契约。&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;div style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&#10;&lt;table style="border-spacing:0;padding:0;margin:0;border:0;"&gt;&lt;tr&gt;&lt;td style="vertical-align:top;padding:0;margin:0;border:0;"&gt;&#10;&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"&gt; 1&#10;&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"&gt; 2&#10;&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"&gt; 3&#10;&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"&gt; 4&#10;&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"&gt; 5&#10;&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"&gt; 6&#10;&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"&gt; 7&#10;&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"&gt; 8&#10;&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"&gt; 9&#10;&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"&gt;10&#10;&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"&gt;11&#10;&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"&gt;12&#10;&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"&gt;13&#10;&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"&gt;14&#10;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#10;&lt;td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%"&gt;&#10;&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#8b949e;font-style:italic"&gt;// alchemy-furnace 网关侧的技能包接口&lt;/span&gt;&lt;span style="color:#6e7681"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;r.&lt;span style="color:#d2a8ff;font-weight:bold"&gt;POST&lt;/span&gt;(&lt;span style="color:#a5d6ff"&gt;&amp;#34;/api/skills&amp;#34;&lt;/span&gt;,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;func&lt;/span&gt;(c&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;*&lt;/span&gt;gin.Context)&lt;span style="color:#6e7681"&gt; &lt;/span&gt;{&lt;span style="color:#6e7681"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;var&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;req&lt;span style="color:#6e7681"&gt; &lt;/span&gt;CreateSkillReq&lt;span style="color:#6e7681"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;if&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;err&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;:=&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;c.&lt;span style="color:#d2a8ff;font-weight:bold"&gt;ShouldBindJSON&lt;/span&gt;(&lt;span style="color:#ff7b72;font-weight:bold"&gt;&amp;amp;&lt;/span&gt;req);&lt;span style="color:#6e7681"&gt; &lt;/span&gt;err&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;!=&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#79c0ff"&gt;nil&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;{&lt;span style="color:#6e7681"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;c.&lt;span style="color:#d2a8ff;font-weight:bold"&gt;JSON&lt;/span&gt;(&lt;span style="color:#a5d6ff"&gt;400&lt;/span&gt;,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;gin.H{&lt;span style="color:#a5d6ff"&gt;&amp;#34;error&amp;#34;&lt;/span&gt;:&lt;span style="color:#6e7681"&gt; &lt;/span&gt;err.&lt;span style="color:#d2a8ff;font-weight:bold"&gt;Error&lt;/span&gt;()})&lt;span style="color:#6e7681"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;return&lt;/span&gt;&lt;span style="color:#6e7681"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;}&lt;span style="color:#6e7681"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;skill,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;err&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;:=&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;skillSvc.&lt;span style="color:#d2a8ff;font-weight:bold"&gt;Create&lt;/span&gt;(c.Request.&lt;span style="color:#d2a8ff;font-weight:bold"&gt;Context&lt;/span&gt;(),&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;&amp;amp;&lt;/span&gt;req)&lt;span style="color:#6e7681"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;if&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;err&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72;font-weight:bold"&gt;!=&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#79c0ff"&gt;nil&lt;/span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;{&lt;span style="color:#6e7681"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;c.&lt;span style="color:#d2a8ff;font-weight:bold"&gt;JSON&lt;/span&gt;(&lt;span style="color:#a5d6ff"&gt;500&lt;/span&gt;,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;gin.H{&lt;span style="color:#a5d6ff"&gt;&amp;#34;error&amp;#34;&lt;/span&gt;:&lt;span style="color:#6e7681"&gt; &lt;/span&gt;err.&lt;span style="color:#d2a8ff;font-weight:bold"&gt;Error&lt;/span&gt;()})&lt;span style="color:#6e7681"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;&lt;span style="color:#ff7b72"&gt;return&lt;/span&gt;&lt;span style="color:#6e7681"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;}&lt;span style="color:#6e7681"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6e7681"&gt; &lt;/span&gt;c.&lt;span style="color:#d2a8ff;font-weight:bold"&gt;JSON&lt;/span&gt;(&lt;span style="color:#a5d6ff"&gt;200&lt;/span&gt;,&lt;span style="color:#6e7681"&gt; &lt;/span&gt;skill)&lt;span style="color:#6e7681"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;})&lt;span style="color:#6e7681"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;/div&gt;&lt;p&gt;第二步，用 Cursor 的 Composer 搭骨架。我描述需求：&amp;ldquo;用 Next.js App Router + shadcn/ui + Tailwind，做一个三栏布局，左侧技能包列表，中间编辑器，右侧预览&amp;rdquo;，它生成初始代码后我再逐块调整。这里的关键是给足上下文：把 Swagger 文档、设计参考截图、已有的组件目录一起喂给它。&lt;/p&gt;&#10;&lt;p&gt;第三步，复杂交互用 Claude Code 细抠。比如融合血统的树形可视化（Promptbreeder 变异算子 + 血统追溯），我在 Claude Code 里打开相关组件，直接说&amp;quot;这里要改成 DAG 布局，节点颜色按代数区分，点击节点展示该代的 Prompt 全文&amp;quot;，它会读现有代码再精准改。&lt;/p&gt;&#10;&lt;p&gt;&lt;img alt="Vibe Coding 全栈闭环：定契约、搭骨架、细抠交互，外加一条截图回修的反馈回路" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://yusanwen-code.github.io/images/post-66-vibe-fullstack.svg"&gt;&lt;/p&gt;&#10;&lt;h2 id="五个教训"&gt;五个教训&#10;&lt;/h2&gt;&lt;p&gt;第一，别指望 AI 生成的样式一次到位。布局错乱、响应式断点不对、暗色模式漏色，它经常顾头不顾尾。我的办法是每生成一块就在浏览器里看，不对就截图丢回去让它修。小步快跑，别攒到最后。&lt;/p&gt;&#10;&lt;p&gt;第二，状态管理别过度设计。AI 一上来就喜欢套 Zustand/Redux，其实大部分页面用 React Query 管服务端状态、&lt;code&gt;useState&lt;/code&gt; 管本地状态就够了。alchemy-furnace 这种 DEMO 规模，最后我什么全局状态库都没引入。&lt;/p&gt;&#10;&lt;p&gt;第三，类型安全一定要守住。TypeScript strict 模式全开，API 响应类型从 OpenAPI 生成（&lt;code&gt;openapi-typescript&lt;/code&gt;），不让 AI 随手写 &lt;code&gt;any&lt;/code&gt;。后端改字段，前端编译就能发现，比联调时互相甩锅高效太多。&lt;/p&gt;&#10;&lt;p&gt;第四，UI 库选 shadcn/ui 这类可复制的。组件源码直接进你的仓库，想怎么改都行，比黑盒组件库适合 Vibe Coding。AI 对它的代码也最熟，生成质量明显更高。&lt;/p&gt;&#10;&lt;p&gt;第五，后端思维得切换。后端是&amp;quot;请求-响应&amp;quot;，前端是&amp;quot;渲染是状态的函数&amp;quot;。一开始我总在 &lt;code&gt;useEffect&lt;/code&gt; 里绕圈子，后来想明白这一点，写起来就顺了。该花半天过一遍 React 官方文档，比让 AI 反复擦屁股强。&lt;/p&gt;&#10;&lt;h2 id="后来"&gt;后来&#10;&lt;/h2&gt;&lt;p&gt;Vibe Coding 没有把前端变成&amp;quot;不需要学&amp;quot;，只是把门槛从&amp;quot;记住所有 API&amp;quot;降到了&amp;quot;理解核心概念 + 会描述需求 + 能判断对错&amp;quot;。&lt;/p&gt;&#10;&lt;p&gt;后端工程师独立交付前端的最大红利，是契约和实现都能自己掌控，联调成本归零。alchemy-furnace 上线后我反而觉得，至少在产品快速迭代期，这种全栈闭环比前后端分工还快。&lt;/p&gt;&#10;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;封面图：&lt;a class="link" href="https://www.flickr.com/photos/38869431@N00/9791626204" target="_blank" rel="noopener"&#10; &gt;juhansonin / Flickr&lt;/a&gt; · CC BY 2.0&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;</description></item></channel></rss>