乐于分享
好东西不私藏

AI代码助手技术栈大比拼:React、Monaco与CodeMirror的选择之道

AI代码助手技术栈大比拼:React、Monaco与CodeMirror的选择之道
博主介绍

🌏 了解博主:波仔椿

📄 组件拆得够小,页面才装得下变化。


文章内容

最近不少人在问我:想做一个类似 Codex、Cursor 这种 AI 代码助手,前端到底该选哪些技术?说实话,这个问题我去年也纠结过,当时把市面上几款主流产品的源码和技术栈扒了一遍,才慢慢理清思路。今天就把我踩过的坑、验证过的方案一次讲透。

一、先搞清楚:Codex 类产品到底要解决什么

你想想,一个 AI 代码助手,用户打开它最想干啥?无非这几件事:跟 AI 聊需求、让 AI 直接改代码、看 AI 改了哪些地方、确认要不要接受改动。拆开来就是六个核心能力

1.AI 对话:多轮聊天,支持上下文2.流式输出:AI 回答要一个字一个字蹦出来,不能等整段才显示3.代码编辑器:能看代码、能改代码,最好有语法高亮和智能提示4.Diff 视图:展示 AI 改了哪些行,红绿对比5.工具调用:AI 能调你写的函数,比如读文件、跑命令6.多文件管理:项目里几十个文件,得能切换、能搜索

这六个能力,决定了你前端要选什么框架、什么编辑器组件、什么状态管理。先看整体架构长啥样:

用户交互层聊天输入框 · 代码编辑器 · Diff 接受/拒绝按钮 · 文件树UI 框架层React 18 / Vue 3 + 构建工具 Vite / Next.js代码编辑器Monaco Editor / CodeMirror 6流式 Markdown 渲染streamdown / react-markdown + Shiki状态管理层Zustand / Redux Toolkit (React) · Pinia (Vue)AI 流式通信层Vercel AI SDK (useChat) · SSE 数据流协议 · 工具调用后端 API:LLM 代理 · 文件系统 · 工具执行

Codex 类产品前端架构分层:从用户操作到后端 AI,每一层都有对应的技术选型

说白了,选型的本质就是回答六个问题:用什么框架、用什么编辑器、怎么流式渲染、怎么做 Diff、怎么管状态、怎么跟 AI 通信。下面逐个拆。

二、扒一扒四款主流产品的技术底牌

选型最怕闭门造车,不如先看看别人怎么做的。我把 Cursor、Continue.dev、Cody、Claude Code 这四款的底牌翻了出来:

1. Cursor —— VS Code 的"魔改版"

Cursor 是直接 fork 了 VS Code 整个壳子,所以它的技术栈几乎就是 VS Code 的技术栈:Electron 做桌面壳,Monaco 做编辑器,连 UI 都不是 React 写的,而是 VS Code 自己那套原生 TypeScript 组件系统。Diff 视图直接用 Monaco 内置的 diff editor。说白了,Cursor 不是"用前端框架搭出来的",是"把一个现成 IDE 改出来的"。这条路子个人开发者基本走不通——你不可能 fork 一整个 VS Code。

2. Continue.dev —— 最值得参考的开源样本

Continue 是开源的,源码在 GitHub 上能直接看。它的前端用React 18 + Vite构建,跑在 VS Code 的 webview 里。编辑器用 Monaco(声明的是 0.53 以上版本),状态管理用的是Redux Toolkit,富文本输入框用 TipTap,样式用 styled-components。AI 流式输出这块,它特意做了个"透传"架构:webview 不直接连 LLM,而是由 Core 层负责跟模型通信,再把 token 转发给界面层。这个设计很值得学——避免前端直接持有 API 密钥。

3. Cody(Sourcegraph)—— React + 自建设计系统

Cody 的 Web 版是 React + TypeScript,组件库用的是 Sourcegraph 内部的 Wildcard 设计系统。有个细节挺有意思:它的语法高亮不是在浏览器里跑的,而是后端用 Rust 写的 Syntect 服务(基于 Sublime Text 的语法文件)。这么做是为了让高亮和后端搜索结果一致,但对你来说未必需要这么重。Cody 在 VS Code 里的版本则复用宿主的 Monaco。

4. Claude Code —— React 渲染到终端

这个最反直觉:Claude Code 是个命令行工具,但它居然是用React 18 + Ink写的。Ink 是个把 React 渲染到终端的渲染器,配 Yoga 做 flexbox 布局。运行时用 Bun,类型校验用 Zod v4,命令解析用 Commander.js。它没有 Monaco、没有网页,但照样把流式输出、工具调用玩得转。这告诉我们一件事:React 的渲染层是可以换的,关键是你那套组件逻辑能不能复用。

汇总对比一下:

维度
Cursor
Continue.dev
Cody Web
Claude Code
框架
VS Code 原生 TS
React 18
React
React 18 + Ink
编辑器
Monaco
Monaco
Monaco(宿主)
无(终端)
状态管理
VS Code DI
Redux Toolkit
React hooks
React hooks
构建工具
gulp/esbuild
Vite
pnpm 工作区
Bun
运行形态
Electron 桌面
IDE webview
浏览器
终端 CLI

看明白没?除了 Cursor 这种 fork 路子,剩下三款全是 React 系。Continue.dev 是最接近"从零搭一个"的参考样本:React + Vite + Monaco + Redux Toolkit,这套组合被验证过能跑通。


三、前端框架:React、Vue 还是 Svelte

这个问题的答案在上一节其实已经露出水面了。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

这俩不是同一层东西,但经常被放一起选。简单讲:Vite 是纯前端构建工具,Next.js 是全栈框架(带 API 路由、SSR)。怎么选取决于你的后端打算怎么搞。

维度
Vite
Next.js
定位
纯前端构建 + dev server
全栈框架(含 API 路由)
Monaco 兼容
原生友好
需 next/dynamic 关 SSR
后端 API
另起 Hono / Express
route.ts 原生支持
冷启动速度
较慢
适合形态
纯前端 SPA / IDE 形态
需鉴权 + SSR + 一体化

我的建议:做桌面 IDE 形态或纯前端 SPA,选 Vite,后端另起一个轻量服务(Hono 或 Express)专门代理 LLM。如果你想把前后端塞一个仓库、需要登录态、需要 SSR,再考虑 Next.js——但要做好 Monaco 不支持 SSR、得用 next/dynamic 动态导入的心理准备,或者干脆换 CodeMirror 6(它原生 SSR 友好)。


五、代码编辑器:Monaco 还是 CodeMirror 6

这是整篇文章最关键的一个选择。两个都是顶级的代码编辑器组件,但性格差很多。

Monaco Editor,就是 VS Code 用的那个引擎,微软出品。优点是 IntelliSense 天花板级别,TypeScript 语言服务几乎是浏览器里能拿到最好的;内置 DiffEditor,做 AI 改动对比不用再找第三方库;多文件 tab、minimap、代码折叠全都有。缺点是体积大(解压后近百 MB),不支持 SSR,移动端体验一般。

CodeMirror 6,作者 Marijn Haverbeke(《Eloquent JavaScript》那位)。优点是模块化、体积小(按需引入)、SSR 友好、移动端支持好、无障碍做得扎实,支持 130 多种语言。缺点是没有内置 diff,IDE 级能力得自己用扩展拼。

维度
Monaco
CodeMirror 6
出品
微软(VS Code 同款)
Marijn Haverbeke
IntelliSense
天花板级
需扩展拼
内置 Diff
无(需插件)
SSR
不支持
支持
体积
小(按需)
移动端
一般
React 封装
@monaco-editor/react 4.7.0
@uiw/react-codemirror 4.25.x

结论很直接:做 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',inherittrue,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 基本零配置。


六、AI 流式渲染:Vercel AI SDK 怎么工作

这是 Codex 类产品的灵魂——AI 回答得一个字一个字往外蹦,不是等整段返回。这套机制现在是 Vercel AI SDK 的事实标准。当前主版本是ai 7.x(要求 Node 22 以上),配套的 React hooks 在@ai-sdk/react里。

useChat:一个 hook 搞定流式聊天

useChat帮你管了四件事:消息列表、输入状态、流式状态(ready / submitted / streaming / error)、发送。注意 v7 的 API 跟老教程不一样:发消息用sendMessage({ text }),不是老版本的handleSubmit。消息结构也变了,每条消息有parts数组,文本、工具调用、图片各自是一个 part。

底层协议:SSE 数据流

流式传输走的是 HTTP + ReadableStream,数据用 SSE(Server-Sent Events)格式编码。默认的"数据流协议"会发一串带类型的事件:text-delta是文本增量、tool-input-delta是工具调用参数增量、tool-output-available是工具执行结果。流结束发一个[DONE]标记。这套协议天然支持工具调用——这正是 Codex 类产品要的。

用户输入sendMessage()useChat@ai-sdk/react后端 route.tsstreamText()LLMOpenAI/ClaudeSSE 数据流(ReadableStream)text-delta · tool-input-delta · tool-output-available · [DONE]message.parts 增量渲染文本 → Streamdown · 工具调用 → ToolCard · 结果 → DiffView

AI SDK 流式数据流:从用户输入到 parts 增量渲染的完整链路

流式 Markdown 渲染:streamdown

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 组件。


七、Diff 视图:展示 AI 改了哪些代码

AI 说"我把第 30 行改了",用户得能看见改了啥。这就是 Diff 视图的活。两条路:

路子一:Monaco 内置 DiffEditor。你已经用了 Monaco,直接用它的DiffEditor组件,传originalmodified两段代码就行,支持并排和内联两种视图,用的是 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 做得最顺,值得抄。

八、状态管理:Zustand、Redux Toolkit 还是 Pinia

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 就够了。


九、三套推荐技术栈组合

讲了这么多,给你三套可以直接抄的组合,对应三种产品形态:

方案 A:桌面 IDE 形态(对标 Cursor)

Electron + React 18 + Vite + Monaco Editor + Zustand + Vercel AI SDK后端:Node(Hono/Express)代理 LLM,不把密钥放前端

方案 B:Web SPA 形态(对标 Cody Web)

Vite + React 18 + Monaco + Zustand + streamdown + @ai-sdk/react后端:独立 API 服务(Hono),处理鉴权和 LLM 代理

方案 C:Next.js 全栈形态

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 能显示红绿对比。


精彩推荐

Vue3全屏实战:一键搞定数据大屏

Vue3深色模式实现方案:一键切换零白屏

Vue3组件封装:从混乱到高复用的实战指南

Vue3国际化实战:vue-i18n避坑指南

Vue3 + AI:打造智能内容创作后台的实战经验分享

你在做 AI 代码助手的时候,卡在哪一层了?是 Monaco 的 worker 配置炸了,还是 AI SDK v7 的 API 变化把你坑了?来评论区聊聊,说不定你踩的坑正好也是别人正在头疼的。

觉得有用,点个赞、分享给需要的朋友~

❤️ 点赞    📤 分享    ⭐ 喜欢


本篇文章唯一版权归属©波仔椿