
🌏 了解博主:波仔椿
📄 组件拆得够小,页面才装得下变化。
最近不少人在问我:想做一个类似 Codex、Cursor 这种 AI 代码助手,前端到底该选哪些技术?说实话,这个问题我去年也纠结过,当时把市面上几款主流产品的源码和技术栈扒了一遍,才慢慢理清思路。今天就把我踩过的坑、验证过的方案一次讲透。
你想想,一个 AI 代码助手,用户打开它最想干啥?无非这几件事:跟 AI 聊需求、让 AI 直接改代码、看 AI 改了哪些地方、确认要不要接受改动。拆开来就是六个核心能力:
1.AI 对话:多轮聊天,支持上下文2.流式输出:AI 回答要一个字一个字蹦出来,不能等整段才显示3.代码编辑器:能看代码、能改代码,最好有语法高亮和智能提示4.Diff 视图:展示 AI 改了哪些行,红绿对比5.工具调用:AI 能调你写的函数,比如读文件、跑命令6.多文件管理:项目里几十个文件,得能切换、能搜索
这六个能力,决定了你前端要选什么框架、什么编辑器组件、什么状态管理。先看整体架构长啥样:
Codex 类产品前端架构分层:从用户操作到后端 AI,每一层都有对应的技术选型
说白了,选型的本质就是回答六个问题:用什么框架、用什么编辑器、怎么流式渲染、怎么做 Diff、怎么管状态、怎么跟 AI 通信。下面逐个拆。
选型最怕闭门造车,不如先看看别人怎么做的。我把 Cursor、Continue.dev、Cody、Claude Code 这四款的底牌翻了出来:
Cursor 是直接 fork 了 VS Code 整个壳子,所以它的技术栈几乎就是 VS Code 的技术栈:Electron 做桌面壳,Monaco 做编辑器,连 UI 都不是 React 写的,而是 VS Code 自己那套原生 TypeScript 组件系统。Diff 视图直接用 Monaco 内置的 diff editor。说白了,Cursor 不是"用前端框架搭出来的",是"把一个现成 IDE 改出来的"。这条路子个人开发者基本走不通——你不可能 fork 一整个 VS Code。
Continue 是开源的,源码在 GitHub 上能直接看。它的前端用React 18 + Vite构建,跑在 VS Code 的 webview 里。编辑器用 Monaco(声明的是 0.53 以上版本),状态管理用的是Redux Toolkit,富文本输入框用 TipTap,样式用 styled-components。AI 流式输出这块,它特意做了个"透传"架构:webview 不直接连 LLM,而是由 Core 层负责跟模型通信,再把 token 转发给界面层。这个设计很值得学——避免前端直接持有 API 密钥。
Cody 的 Web 版是 React + TypeScript,组件库用的是 Sourcegraph 内部的 Wildcard 设计系统。有个细节挺有意思:它的语法高亮不是在浏览器里跑的,而是后端用 Rust 写的 Syntect 服务(基于 Sublime Text 的语法文件)。这么做是为了让高亮和后端搜索结果一致,但对你来说未必需要这么重。Cody 在 VS Code 里的版本则复用宿主的 Monaco。
这个最反直觉:Claude Code 是个命令行工具,但它居然是用React 18 + Ink写的。Ink 是个把 React 渲染到终端的渲染器,配 Yoga 做 flexbox 布局。运行时用 Bun,类型校验用 Zod v4,命令解析用 Commander.js。它没有 Monaco、没有网页,但照样把流式输出、工具调用玩得转。这告诉我们一件事:React 的渲染层是可以换的,关键是你那套组件逻辑能不能复用。
汇总对比一下:
看明白没?除了 Cursor 这种 fork 路子,剩下三款全是 React 系。Continue.dev 是最接近"从零搭一个"的参考样本:React + Vite + Monaco + Redux Toolkit,这套组合被验证过能跑通。
这个问题的答案在上一节其实已经露出水面了。AI 代码助手这个赛道,React 是事实标准。原因很现实:Vercel AI SDK 官方最先支持 React(@ai-sdk/react),Monaco 的 React 封装最成熟(@monaco-editor/react),社区里能抄的轮子几乎全是 React 写的。Continue、Cody、Claude Code 全是 React。
那 Vue 能不能做?能。Vue 3 + Pinia 完全跑得通,AI SDK 也出了@ai-sdk/vue官方包。但你要做好心理准备:遇到问题搜中文社区,Vue 版的答案比 React 版少一个量级。如果你团队本来就熟 Vue,硬上 React 不划算;如果是从零开始,选 React 少踩坑。
Svelte 呢?轻量是真轻量,但生态在这类"重交互+重状态"的场景里偏薄,不推荐作为主力。除非你做的只是一个轻量对话面板。
结论:新项目无脑选 React 18;团队已有 Vue 深度积累就用 Vue 3 + Pinia,配 @ai-sdk/vue。Svelte 暂时不碰。
这俩不是同一层东西,但经常被放一起选。简单讲:Vite 是纯前端构建工具,Next.js 是全栈框架(带 API 路由、SSR)。怎么选取决于你的后端打算怎么搞。
我的建议:做桌面 IDE 形态或纯前端 SPA,选 Vite,后端另起一个轻量服务(Hono 或 Express)专门代理 LLM。如果你想把前后端塞一个仓库、需要登录态、需要 SSR,再考虑 Next.js——但要做好 Monaco 不支持 SSR、得用 next/dynamic 动态导入的心理准备,或者干脆换 CodeMirror 6(它原生 SSR 友好)。
这是整篇文章最关键的一个选择。两个都是顶级的代码编辑器组件,但性格差很多。
Monaco Editor,就是 VS Code 用的那个引擎,微软出品。优点是 IntelliSense 天花板级别,TypeScript 语言服务几乎是浏览器里能拿到最好的;内置 DiffEditor,做 AI 改动对比不用再找第三方库;多文件 tab、minimap、代码折叠全都有。缺点是体积大(解压后近百 MB),不支持 SSR,移动端体验一般。
CodeMirror 6,作者 Marijn Haverbeke(《Eloquent JavaScript》那位)。优点是模块化、体积小(按需引入)、SSR 友好、移动端支持好、无障碍做得扎实,支持 130 多种语言。缺点是没有内置 diff,IDE 级能力得自己用扩展拼。
结论很直接:做 Cursor / Continue 那种桌面 IDE 感的,选 Monaco,IntelliSense 和内置 Diff 正好匹配需求。如果是 Next.js 全栈、或者要做移动端友好的轻量助手,选 CodeMirror 6,SSR 不用绕弯。
下面是 Monaco 在 React 里的接入方式,核心就一个@monaco-editor/react组件:
import { useRef } from'react'import Editor from'@monaco-editor/react'// Monaco 编辑器封装:支持多文件 tab、自定义主题、取值// @monaco-editor/react 4.7.0 默认从 CDN 加载 monaco-editor,无需额外 webpack 配置exportdefaultfunctionCodeEditor({ filePath, language = 'typescript', defaultValue = '' }) {const editorRef = useRef(null)functionhandleMount(editor, monaco) { editorRef.current = editor// 自定义暗色主题,跟 AI 助手的深色界面统一 monaco.editor.defineTheme('aiDark', {base: 'vs-dark',inherit: true,rules: [ { token: 'comment', foreground: '6a9955', fontStyle: 'italic' }, { token: 'keyword', foreground: '569cd6' } ],colors: {'editor.background': '#1e1e2e' } }) monaco.editor.setTheme('aiDark') }// 外部通过 ref 调用 getValue() 拿到当前文件内容functiongetValue() {return editorRef.current?.getValue() ?? '' }return (<Editorheight="70vh"defaultLanguage={language}defaultValue={defaultValue}path={filePath}theme="aiDark"onMount={handleMount}options={{fontSize:14,minimap: { enabled:true },automaticLayout:true,scrollBeyondLastLine:false,tabSize:2,wordWrap: 'on' }} /> )}几个关键点:path属性用来支持多文件 tab(每个文件一个 model);onMount拿到 editor 实例后可以 defineTheme 自定义主题;取值用editor.getValue()。默认从 CDN 加载,Vite 基本零配置。
这是 Codex 类产品的灵魂——AI 回答得一个字一个字往外蹦,不是等整段返回。这套机制现在是 Vercel AI SDK 的事实标准。当前主版本是ai 7.x(要求 Node 22 以上),配套的 React hooks 在@ai-sdk/react里。
useChat帮你管了四件事:消息列表、输入状态、流式状态(ready / submitted / streaming / error)、发送。注意 v7 的 API 跟老教程不一样:发消息用sendMessage({ text }),不是老版本的handleSubmit。消息结构也变了,每条消息有parts数组,文本、工具调用、图片各自是一个 part。
流式传输走的是 HTTP + ReadableStream,数据用 SSE(Server-Sent Events)格式编码。默认的"数据流协议"会发一串带类型的事件:text-delta是文本增量、tool-input-delta是工具调用参数增量、tool-output-available是工具执行结果。流结束发一个[DONE]标记。这套协议天然支持工具调用——这正是 Codex 类产品要的。
AI SDK 流式数据流:从用户输入到 parts 增量渲染的完整链路
AI 边输出边渲染 markdown 有个坑:代码块可能还没闭合(``` 只写了一半),普通 markdown 解析器会炸。Vercel 自己出了个streamdown(当前 2.5.0),专门解决这个问题——它能优雅处理未闭合的代码块,内置 Shiki 语法高亮,支持 GFM、KaTeX 公式、Mermaid 图。它就是官方 AI Elements 组件背后用的渲染器,跟 useChat 是配套的。
下面是一个完整的聊天面板,useChat + streamdown 配合:
import { useState } from'react'import { useChat } from'@ai-sdk/react'import { Streamdown } from'streamdown'import'streamdown/styles.css'// AI SDK v7 的 useChat:自动管理消息列表、流式状态、输入// streamdown 专门为流式 markdown 设计,能处理未闭合的代码块exportdefaultfunctionChatPanel() {const [input, setInput] = useState('')const { messages, status, sendMessage } = useChat({api: '/api/chat' })asyncfunctionhandleSubmit(e) { e.preventDefault()if (!input.trim() || status !== 'ready') returnawait sendMessage({ text: input }) setInput('') }return (<divclassName="chat-panel"><divclassName="message-list"> {messages.map((message) => (<divkey={message.id}className={`messagemessage-${message.role}`}><strong>{message.role === 'user' ? '你' : 'AI'}</strong> {message.parts.map((part, index) => { if (part.type === 'text') { return (<Streamdownkey={index}animatedisAnimating={status === 'streaming'} > {part.text}</Streamdown> ) } return null })}</div> ))}</div><formonSubmit={handleSubmit}className="input-form"><textareavalue={input}onChange={(e) => setInput(e.target.value)} placeholder="输入你的需求,比如「把 App.tsx 里的 useEffect 换成 useEvent」" rows={3} /><buttontype="submit"disabled={status !== 'ready'}> {status === 'streaming' ? '生成中…' : '发送'}</button></form></div> )}两个细节:一是isAnimating={status === 'streaming'}告诉 streamdown 正在流式输出,它会做未闭合处理;二是遍历message.parts时按part.type分发——文本走 Streamdown,工具调用走你自己的 ToolCard 组件。
AI 说"我把第 30 行改了",用户得能看见改了啥。这就是 Diff 视图的活。两条路:
路子一:Monaco 内置 DiffEditor。你已经用了 Monaco,直接用它的DiffEditor组件,传original和modified两段代码就行,支持并排和内联两种视图,用的是 advanced diff 算法。Cursor 和 Continue 用的就是这个。
路子二:react-diff-viewer-continued。如果你没用 Monaco(比如选了 CodeMirror 6),这个库是 React 生态里维护得最好的独立 diff 组件(4.4.0 版本,还在更新),支持逐词对比、分屏/合并视图。底层算 diff 可以用 Google 的 diff-match-patch(1.0.5,稳定但好几年没更新了)。
Monaco DiffEditor 的接入也很简单:
import { DiffEditor } from '@monaco-editor/react'// Monaco 内置 DiffEditor:展示 AI 改了哪些代码,支持并排/内联两种视图// 这是 Cursor、Continue.dev 用的同一套 diff 能力export defaultfunctionCodeDiffViewer({ original, modified, language = 'typescript'}){return ( <DiffEditor height="60vh" language={language} original={original} modified={modified} theme="vs-dark" options={{ readOnly: true, renderSideBySide: true, originalEditable: false, automaticLayout: true, diffWordWrap: 'on' }} /> )}// 使用示例:// <CodeDiffViewer// original={oldCode}// modified={aiGeneratedCode}// language="typescript"// />经验:AI 改代码时,先让用户看 diff,再给"接受/拒绝"两个按钮。接受了才把 modified 写回编辑器。这个交互 Cursor 做得最顺,值得抄。
AI 代码助手的状态不算复杂:消息列表、当前打开的文件、文件树、流式状态、工具调用结果。说白了就是几个 slice。这块选型没有唯一解。
Zustand(5.0.15):pmndrs 出品,API 就一个create()hook,没有 boilerplate,没有 Provider 包裹。新项目我最推荐这个。
Redux Toolkit:Continue.dev 在用。如果你团队 Redux 用得熟、需要时间旅行调试,可以选。但说实话对这种应用有点重。
Pinia(4.0.3):Vue 阵营官方推荐,类型安全、模块化,配 Vue 3 天然契合。选 Vue 就用它。
Zustand 管理聊天 + 文件状态的写法:
import { create } from'zustand'// Zustand 管理聊天会话、当前文件、流式状态// 比 Redux Toolkit 轻量,API 就一个 hook,适合 IDE 形态的应用exportconst useChatStore = create((set) => ({ messages: [], activeFile: null, fileTree: [], isStreaming: false,// 追加一条完整消息 addMessage: (message) =>set((state) => ({ messages: [...state.messages, message] })),// 流式输出时往最后一条消息追加内容 appendToLastMessage: (chunk) =>set((state) => {const messages = [...state.messages]const last = messages[messages.length - 1]if (last) { messages[messages.length - 1] = { ...last, content: last.content + chunk } }return { messages } }), setActiveFile: (path) =>set({ activeFile: path }), setFileTree: (tree) =>set({ fileTree: tree }), setStreaming: (streaming) =>set({ isStreaming: streaming }), reset: () =>set({ messages: [], activeFile: null, isStreaming: false })}))关键点:流式输出时别每来一个 token 就 push 一条新消息,而是用appendToLastMessage往最后一条消息追加内容,否则消息列表会爆掉。Jotai(2.20.0)跟 Zustand 是同一个团队出的,适合状态之间有大量派生关系的场景,普通聊天用 Zustand 就够了。
讲了这么多,给你三套可以直接抄的组合,对应三种产品形态:
Electron + React 18 + Vite + Monaco Editor + Zustand + Vercel AI SDK后端:Node(Hono/Express)代理 LLM,不把密钥放前端
Vite + React 18 + Monaco + Zustand + streamdown + @ai-sdk/react后端:独立 API 服务(Hono),处理鉴权和 LLM 代理
Next.js + React 18 + CodeMirror 6 + Zustand + streamdown + AI SDK route.ts后端:Next.js API 路由直连,支持 SSR 和鉴权
方案 B 的依赖清单长这样(版本号都核对过,是当前 npm 上的真实版本):
{"name": "ai-coding-assistant","private": true,"version": "0.1.0","type": "module","scripts": {"dev": "vite","build": "vite build","preview": "vite preview" },"dependencies": {"react": "^18.3.1","react-dom": "^18.3.1","@monaco-editor/react": "^4.7.0","monaco-editor": "^0.56.0","ai": "^7.0.0","@ai-sdk/react": "^4.0.0","@ai-sdk/openai": "^4.0.0","@ai-sdk/anthropic": "^4.0.0","streamdown": "^2.5.0","zustand": "^5.0.0","zod": "^3.25.76" },"devDependencies": {"@vitejs/plugin-react": "^4.3.0","vite": "^7.0.0" }}Vite 配置 Monaco 的注意点(默认走 CDN 零配置,本地化方案也给了注释):
import { defineConfig } from'vite'import react from'@vitejs/plugin-react'// @monaco-editor/react 默认从 CDN 加载 monaco-editor,Vite 几乎零配置// 如果想把 monaco-editor 打进本地 bundle(离线/内网场景),// 需要额外配置 worker,下面注释给出了本地化方案exportdefault defineConfig({ plugins: [react()], worker: { format: 'es' }, optimizeDeps: { include: ['monaco-editor'] }})// === 本地化 Monaco 方案(取消注释使用)===// 需要先安装 vite-plugin-monaco-editor//// import monacoEditor from 'vite-plugin-monaco-editor'//// export default defineConfig({// plugins: [// react(),// monacoEditor({// languageWorkers: ['editor', 'ts', 'json', 'css', 'html']// })// ]// })1.环境依赖:Node 22 以上(AI SDK 7.x 硬性要求),React 18+,Vite 7;Monaco 默认走 CDN 无需额外 worker 配置。
2.步骤完整性:从 npm install 到跑通聊天面板,代码 1-6 串联起来就是一条完整链路:编辑器 → 聊天 → diff → 状态 → 依赖 → 构建。
3.版本匹配:文中所有版本号(ai 7.x、@monaco-editor/react 4.7.0、streamdown 2.5.0、zustand 5.0.15 等)均为 npm 实际在售版本,API 与版本对应。
4.最大坑点:AI SDK v7 的 API 跟 v3/v4 老教程差异大(sendMessage 取代 handleSubmit、message.parts 结构),抄老教程会报错;Monaco 在 Next.js 里要 next/dynamic 关 SSR。
5.验证方式:跑通后,在聊天框输入"写个 hello world",应该能看到 AI 回答逐字出现、代码块带语法高亮;让 AI 改文件后,DiffViewer 能显示红绿对比。
你在做 AI 代码助手的时候,卡在哪一层了?是 Monaco 的 worker 配置炸了,还是 AI SDK v7 的 API 变化把你坑了?来评论区聊聊,说不定你踩的坑正好也是别人正在头疼的。
觉得有用,点个赞、分享给需要的朋友~
❤️ 点赞 📤 分享 ⭐ 喜欢
本篇文章唯一版权归属©波仔椿
夜雨聆风