返回探索
指南VibeFix 编辑部更新于 2026年10月16日

别让 AI 生成一坨 contenteditable:vibe 项目的富文本编辑器集成实战

AI 200 行就能跑起一个富文本编辑器 demo,但粘贴清洗、移动端键盘、Markdown 往返才是真正的坑。本文给一人团队一条实战路线:Tiptap/BlockNote 选型四维打分、JSON 存储与 Markdown 导出组合、Next.js SSR 水合解法、白名单粘贴清洗管线、图片直传链路,以及上线前 10 项检查表。

Tiptap 编辑器界面截图:工具栏与文档编辑区

划界:本篇讲的是编辑器本身的技术集成,与站内三篇明确分工:vibe-project-help-center-knowledge-base-guide(帮助中心指南)讲的是"帮助中心的内容运营",本篇讲的是"编辑器组件怎么选、怎么接"——如果你要给帮助中心自建编辑器,用的正是本篇的方案;docs-written-for-ai-agents(给 AI 写文档)讲的是"给 AI 看的文档怎么写",本篇讲的是"给人用的编辑器怎么做",读者对象相反;图片上传链路复用 vibe-project-file-storage-cdn-guide(文件存储 CDN)的对象存储方案。

你让 AI 给 vibe 项目加个"能写文章的编辑器",它大概率 200 行就交差:一个 contenteditable 的 div,配上加粗斜体按钮,demo 里看着能跑。你点点头上线了。然后用户从 Word 粘进来一段带 <o:p> 标签和 mso- 内联样式的脏 HTML,编辑器直接原样渲染,样式炸穿全站;有人在 iPhone 上点开,键盘弹起把工具栏顶出屏幕,关掉键盘光标丢了;你想把内容导出成 Markdown 发博客,发现粘回去的表格全碎了。富文本编辑器是 vibe 项目里"最像能用 AI 一把梭、实则坑最密"的模块——它的问题永远不在 demo 阶段,而在真实用户的粘贴板和手机键盘里。本文给一人团队一条实战路线:选型怎么拍板、数据存什么形态、粘贴怎么洗、图片怎么传、上线前查什么,全部落到能直接抄的代码和清单。

一、选型决策表:四款路线的四维打分

一人团队做编辑器,候选路线就四条:Tiptap、BlockNote、Slate、自研 contenteditable。先说结论再摆打分——默认选 Tiptap 或 BlockNote,不要碰另外两条。理由一句话:编辑器的复杂度不在渲染,而在光标、选区、撤销栈、IME 输入法这四座大山上,成熟库替你填了 90% 的坑,你自己填不起。

截至 2026-10-16 亲核官网:Tiptap(tiptap.dev)定位"headless 开源编辑器框架",提供 100+ 扩展和免费/付费 UI 组件、即用模板,仍在活跃维护;BlockNote(blocknotejs.org)自称周安装 10 万+ 的开源 React 块编辑器,底层基于 ProseMirror 与 Yjs,核心编辑器免费,AI 集成、PDF/Word/ODT 导出、多列布局走付费的 XL 包。两者核心 API 面截至当日未见大变,具体版本号以官网为准。

四维打分(5 分制,一人团队视角)

  • Tiptap:生态 5(ProseMirror 生态 + 官方扩展市场,遇到怪问题搜得到答案);可维护性 4(headless 设计,UI 自己拼,自由度高但意味着你要写工具栏);包体积 3(按需引入扩展,基础包不算小);移动端表现 4(ProseMirror 底子,IME 处理成熟)。适合:需要定制工具栏、主题、特殊节点的项目,比如博客后台、文档站。
  • BlockNote:生态 4(React 专属,社区小于 Tiptap 但增速快);可维护性 5(块编辑器,开箱即用的 Notion 式 UI,一人团队最快出效果);包体积 3(同 ProseMirror 系);移动端表现 4。适合:想要"装上就能用、长得像 Notion"的笔记/文档类 vibe 项目。
  • Slate:生态 3、可维护性 2(框架式 API,自由度最高但你要自己实现撤销、序列化、粘贴处理,官方文档都承认它是"框架"不是"成品");包体积 4;移动端表现 2(移动端 IME 与 Android 组合输入是 Slate 的历史重灾区)。一人团队直接 pass——除非你的编辑器有 Slate 才能表达的异形交互。
  • 自研 contenteditable:生态 1、可维护性 1、包体积 5、移动端表现 1。永远不要选。AI 生成的自研编辑器代码能通过 demo 的原因是:demo 只覆盖了"输入英文、点加粗"这条 happy path。真实世界的 execCommand 在各浏览器行为不一致、IME 输入会打断你的手动 DOM 操作、撤销栈要自己维护——这些坑的修复成本是"数周"量级,不是"调几个 prompt"能解决的。

拍板规则:React 技术栈 + 想快 → BlockNote;要深度定制或非 React → Tiptap;纠结这两者时,问自己一个问题——"我的编辑器需要长得不像 Notion 吗?"需要 → Tiptap,不需要 → BlockNote。选定之后就不要再换,编辑器是迁移成本最高的组件之一。

二、文档模型三形态:JSON、HTML、Markdown 存哪个

这是开工前必须拍板的架构决策,改起来比换编辑器还疼——因为它决定你的数据库里躺的是什么。三种形态没有银弹,只有取舍:

Tiptap 编辑器界面截图:工具栏与文档编辑区Tiptap 编辑器界面截图:工具栏与文档编辑区

展开说三条判断依据:

  • 查询需求决定下限。如果你要做"列出所有带代码块的文章""统计每篇的字数和图片数",JSON 文档树可以直接遍历节点类型做结构化查询;HTML 只能全文检索加正则,Markdown 介于两者之间。一人团队常低估这一点:v1 不需要查询,v2 做搜索和推荐时就需要了,而那时迁移几万篇存量数据的成本最高。
  • 迁移成本决定上限。JSON 绑定了编辑器的 schema(Tiptap 的 JSON 和 BlockNote 的块结构互不兼容),换编辑器等于写迁移脚本;Markdown 是纯文本,换谁都不怕。务实的折中是:数据库里只存 JSON(权威版本),同时提供"导出 Markdown"的能力,发布到博客、喂给 AI、做全文索引都用导出的 Markdown。这样权威数据结构化,流通数据轻量化。
  • AI 可读性是 2026 年的新维度。你的内容大概率会被 AI 读:做 RAG、做摘要、做站内问答。Markdown 对 LLM 最友好(token 效率最高、结构自然);JSON 次之(结构清晰但 token 开销大);HTML 最差(标签噪声吃掉大量 token)。这也是"存 JSON、导出 Markdown"组合的另一个理由——导出那一步顺手解决了 AI 消费问题。

一句话拍板:存 JSON,导出 Markdown,HTML 只作为渲染中间产物,绝不入库。入库 HTML 是最差的选择:它既丢了结构(迁移时语义丢失),又带了噪声(AI 难读),体积还最大,三输。

三、最小可用集成:先让编辑器在 Next.js 里活下来

vibe 项目大多是 Next.js + React,编辑器集成第一个拦路虎不是功能,是 SSR 水合。Tiptap/BlockNote 都依赖 document 和选区 API,服务端渲染时会直接报错或水合 mismatch。标准解法只有一条:动态导入并关闭 SSR。

// components/Editor.tsx —— 编辑器组件本身保持纯客户端
"use client";
import { useEditor, EditorContent } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";

export default function Editor({ content, onChange }: {
  content: any; onChange: (json: any) => void;
}) {
  const editor = useEditor({
    extensions: [StarterKit],
    content,
    // 立刻渲染,不等水合:避免 SSR 闪烁
    immediatelyRender: false,
    onUpdate: ({ editor }) => onChange(editor.getJSON()),
  });
  return <EditorContent editor={editor} />;
}

// 页面里动态引入,ssr: false
import dynamic from "next/dynamic";
const Editor = dynamic(() => import("@/components/Editor"), { ssr: false });

三个实战判断:

  • 受控还是非受控?编辑器内部状态极重(选区、撤销栈每秒都在变),不要把编辑器做成全受控组件——每次按键都 setState 回 React 会卡。正确姿势是非受控编辑 + 防抖 onChange:编辑器自己管状态,内容变化防抖 500ms 后把 JSON 抛出来存草稿。AI 生成的代码最爱写成全受控,这是卡顿的第一来源。
  • 协同光标先别碰。Yjs + Hocuspocus 的实时协同看着诱人,但它引入的是另一套复杂度:WebSocket 服务、冲突合并、离线同步。一人团队的正确顺序是:单人编辑先行,把粘贴、图片、导出做扎实;等真有"多人同时改一篇"的用户诉求再加协同。BlockNote 虽然内置了一流的 Yjs 集成,那也是"留好接口",不是"第一天就打开"。
  • 草稿自动保存是刚需不是加分项。编辑器崩溃、用户误关标签页,丢稿一次信任就没了。防抖 onChange 落库 localStorage + 远端草稿表,两份都要有。恢复时以远端为准、localStorage 兜底,冲突了就弹提示让用户选——别替用户做决定。

四、工具栏与 slash menu:AI 的"全功能工具栏"是错的

让 AI 生成编辑器工具栏,它会给你 20 个按钮:加粗、斜体、下划线、删除线、H1 到 H6、引用、代码块、有序无序列表、任务列表、表格、分割线、链接、图片、对齐方式、字体颜色、背景色……看起来很全,实则是灾难。原因有二:第一,每个按钮都是一条测试路径,20 个按钮就是 20 个"粘贴进来会怎样、移动端点不点得到"的未知数;第二,用户根本用不上——Notion 的成功恰恰在于把 90% 的格式操作藏进了 / 命令。

按场景裁剪的最小工具集长这样(博客/文档场景实测够用):

  • 行内:加粗、斜体、行内代码、链接(4 个)
  • 块级:H2、H3、引用、代码块、列表、任务列表(6 个,H1 留给文章标题,编辑器内禁用)
  • 对象:图片、分割线(2 个)

注意被砍掉的东西:下划线(中文排版里下划线≈超链接,用户会困惑)、字体颜色(破坏全站视觉一致性,一人团队没有设计师来收拾)、H4 以下标题(层级越深目录越乱)。工具栏的哲学是替用户做减法——你今天少给一个按钮,明天就少修一个"为什么表格在手机上撑破了"的 bug。

slash menu(输入 / 弹命令菜单)才是效率核心,而且实现不难:监听 / 后的输入做过滤,选中后插入对应节点。给 AI 下 prompt 时不要说"做个 Notion 那样的 slash 菜单",要说:"监听正文中的 / 字符,弹出含 8 个命令(标题2/标题3/待办/代码块/引用/图片/分割线/表格)的浮动菜单,支持上下键选择、回车确认、Esc 关闭,选中后删除 /xxx 文本并插入对应块"。把交互细节写进 prompt,AI 才不会给你一个只能用鼠标点的半成品。

五、粘贴清洗:AI 生成代码最常漏的一层

粘贴是富文本编辑器的"真实世界入口":用户的内容 80% 不是手敲的,是从 Word、Notion、网页、微信公众号粘过来的。AI 生成的编辑器代码几乎从不处理粘贴——默认行为是浏览器把剪贴板的 HTML 原样塞进 contenteditable,于是 Word 的 mso-bidi-font-weight 内联样式、Notion 的嵌套 div、网页的 <script> 和 onclick 全进了你的文档。这不只是样式灾难,更是 XSS 攻击面:任何用户可粘贴的富文本入口,都是存储型 XSS 的候选。

Tiptap 编辑器界面截图:工具栏与文档编辑区Tiptap 编辑器界面截图:工具栏与文档编辑区

清洗管线分三层,每层职责单一:

  • 第一层:解析时丢弃。拿到剪贴板 HTML 后先过 DOM 解析,直接干掉 <script>、<iframe>、<object>、<style> 这类永远不该进文档的标签。这一步的目标是"先保命"。
  • 第二层:白名单 schema 映射。只允许你编辑器支持的节点通过:段落、标题、列表、引用、代码块、加粗/斜体/链接/行内代码、图片。Word 的 <o:p>、Notion 的 data-* 属性、不认识的标签——全部降级为纯文本或丢弃。记住铁律:黑名单总会漏网,白名单默认拒绝。你列"不许什么"永远列不全,列"只许什么"才睡得着觉。Tiptap 的 pasteRule / BlockNote 的 paste 钩子都是干这个的,不要自己写正则洗 HTML。
  • 第三层:属性级消毒。标签合法不代表属性合法:<a href="javascript:alert(1)"> 的 a 在白名单里,但 javascript: 协议必须拦;所有 on* 事件属性、style 里的 expression() 一律剥离。链接只允许 http(s):、mailto:,图片 src 只允许你自己的 CDN 域名和 data:(小图场景)。

最后一条实战经验:拦截要记日志。每次丢弃非法标签/属性时打一条结构化日志(时间、用户、被拦的内容摘要)。这不是为了监控用户,而是为了回看:某天有人批量粘贴带恶意脚本的内容,你能从日志里看到攻击样本,而不是只看到"编辑器好像出过问题"。

六、图片与附件链路:直传对象存储 + 占位 + 失败重试

编辑器里的图片上传,AI 最爱生成的方案是"base64 塞进文档 JSON"。这在 demo 里完美运行,在生产里是慢性毒药:一篇带 5 张手机照片的文章,JSON 体积直奔几十 MB,数据库行膨胀、列表接口变慢、导出 Markdown 时 base64 字符串让 LLM 上下文爆炸。正确的链路只有一条:图片直传对象存储,文档里只存 URL。对象存储的具体选型、签名直传、CDN 加速,复用站内 vibe-project-file-storage-cdn-guide(文件存储 CDN 指南)的方案,本篇只讲编辑器侧的三件事:

  • 上传占位符。用户选中图片的那一刻,编辑器里立刻插入一个带进度条的图片占位节点(本地 blob URL 预览),上传在后台进行。这一步的 UX 价值极大:没有占位符,用户会以为"点了没反应"而重复点击,最后传上去 3 张一样的图。占位节点的状态机只有三种:uploading → done(替换为 CDN URL)/ failed。
  • 失败重试。占位节点失败时不要静默消失——显示"上传失败,点击重试",保留本地预览图,重试按钮复用同一个上传函数。弱网环境下这是最高频的路径,AI 生成的代码里十有八九没有。另外,前端压缩要在上传前做:手机拍的 4MB 照片压到 200KB 以内再传,canvas 一行代码的事,能省掉 90% 的上传失败和 CDN 账单。
  • 删除与孤儿文件。用户删掉图片节点时,对象存储里的文件不会自动消失。v1 可以接受"定期跑脚本清理 7 天未被引用的文件",但要在文档 JSON 里给图片节点打上 fileId,清理脚本才有据可查。别等存储账单吓你一跳才想起这事。

七、Markdown 双向:导入导出与 round-trip 测试

博客、CMS、文档站三类 vibe 项目,Markdown 几乎是刚需:用户习惯用 Markdown 写作、内容要发布到多平台、AI 要读。于是编辑器必须回答两个问题:Markdown 怎么导进来(import)、JSON 怎么导出去(export)。

导出相对简单:遍历 JSON 文档树,按节点类型拼 Markdown。Tiptap 有官方的导出思路、BlockNote 内置了 Markdown 导出,细节在各自文档里。真正的坑在导入:Markdown → 文档树的解析要处理嵌套列表、代码块语言标记、表格对齐,AI 生成的正则解析器会在第一个嵌套列表面前投降。用成熟的 Markdown 解析库(比如 marked / remark)先转 HTML 或 AST,再走你第五节的粘贴清洗管线进文档——复用清洗管线,不要为导入另写一套解析,这是保持行为一致的关键。

round-trip 测试是这一节的交付标准,写法很固定:

// round-trip.test.ts:导出去再导回来,语义不能丢
const cases = [
  "# 标题\n\n段落 **加粗** 和 `代码`",
  "- [ ] 任务一\n- [x] 任务二",
  "| a | b |\n|---|---|\n| 1 | 2 |",
  "```ts\nconst x = 1;\n```",
];
for (const md of cases) {
  const json = markdownToJSON(md);      // 导入
  const back = jsonToMarkdown(json);    // 导出
  // 允许空白差异,不允许语义差异:
  // 标题级别、加粗、任务勾选状态、表格行列数必须一致
  expect(normalize(back)).toBe(normalize(md));
}

测试用例要覆盖你的编辑器支持的每一种节点类型,至少包含:多级标题、嵌套列表、任务列表勾选状态、代码块语言、表格、引用、图片、链接。这个测试文件是编辑器的"契约"——以后升级编辑器版本、换 Markdown 库,先跑它,绿了才敢上线。一人团队没有 QA,这个测试就是你的 QA。

八、上线前检查表:10 项,逐项打勾

编辑器上线前,对着这 10 项逐项验证。每一项背后都是真实用户踩过的坑:

  • 1. iOS 虚拟键盘顶布局。iOS Safari 弹起键盘时不 resize viewport,而是把页面往上顶——固定定位的工具栏会被顶飞或盖住输入区。解法:工具栏用 position: sticky 跟随编辑器容器,或监听 visualViewport 动态调整。必须在真机上测,模拟器复现不了。
  • 2. Android 中文输入法(IME)不断句。Android 上部分输入法的组合输入(composition)会和编辑器的事务冲突,表现为拼音没打完字就上屏了、或者上屏后光标乱跳。Tiptap/ProseMirror 系对 composition 事件处理较成熟,但你的自定义插件(比如 @提及、slash menu)必须在 composition 期间暂停触发——这是 AI 生成代码 100% 会漏的分支。
  • 3. 粘贴三源测试。从 Word 粘一段带样式的、从 Notion 粘一个嵌套列表、从网页粘一篇带链接的文章,三次粘贴后文档结构都必须干净。这是第五节清洗管线的验收标准。
  • 4. XSS 冒烟测试。粘贴 <img src=x onerror=alert(1)> 和 <a href="javascript:alert(1)">,确认脚本不执行、危险属性被剥离。这是安全底线,不是加分项。
  • 5. 大文档性能。往编辑器里灌一篇 2 万字的文章,输入不能有可感知的延迟。如果卡,先查是不是全受控组件(第三节),再查是不是每个节点都绑了过重的 React 组件。
  • 6. 撤销/重做不断链。连续输入、粘贴、删图片、撤销 20 步,文档必须回到可预期的状态。自研编辑器死在这里,成熟库一般没问题——但你的自定义节点(占位图片)要正确接入撤销栈。
  • 7. 图片上传全链路。弱网下传一张 4MB 的手机照片:占位出现、进度走、前压后传、失败可重试、成功后 URL 落库。拿 Chrome DevTools 的 Network 限速到 Slow 3G 测一遍。
  • 8. Markdown round-trip 全绿。第七节的测试文件全绿,且用例覆盖了你支持的所有节点类型。
  • 9. 草稿恢复。写一半关掉标签页重进,内容一字不丢;localStorage 和远端草稿冲突时弹选择框,不静默覆盖。
  • 10. contenteditable 怪癖清单。空段落回车是否产生 <p><br></p>、列表末尾回车是否正确退出列表、标题后回车是否降级为正文——这三处是所有 contenteditable 编辑器的共有怪癖,成熟库处理了 90%,剩下 10% 是你的自定义节点的责任,逐个手动点一遍。

最后说句大实话:富文本编辑器是 vibe 项目里最不值得"vibe"的模块。AI 帮你 200 行跑起 demo 之后,真正的工程量在粘贴清洗、移动端键盘、上传链路这三处——加起来是以周计的工作量。但反过来说,只要你按本文的路线走完:选对库、存 JSON、洗粘贴、传图片、测 round-trip、逐项打勾,你的编辑器就超过了市面上 80% 的 vibe 项目。剩下的 20% 是协同编辑和 CRDT,那是下一篇的故事了。

阅读 0评论 0

评论 (0)

ME
0/1000
评论加载中...
浏览项目广场发布你的项目

相关文章

网页表单截图:字段下方显示内联验证错误信息
指南
别让用户猜哪里填错了:vibe 项目的表单验证与提交实战

每个 vibe 项目都有一堆表单,AI 生成的只会堆 required——模糊的文案、异步竞态、重复提交、丢失的草稿才是用户流失的地方。本文给一人团队完整的表单实战:校验三层分工、错误文案改写、防抖防竞态的异步校验、idle/submitting/success/error 提交状态机与防重提交三保险、文件型表单、多步表单草稿、无障碍,以及上线前 12 项检查表。

用户研究设计体验前端工程
命令面板界面截图:搜索框、命令列表与右侧快捷键提示
指南
效率党的入口:vibe 项目的命令面板(Cmd+K)实战

AI 半小时就能交出一个 Cmd+K 面板,但匹配排序、快捷键冲突、输入法闪烁、移动端降级才是它翻车的地方。本文给一人团队一条完整路线:命令注册表数据模型、五步模糊匹配评分管线、带冲突检测的全局快捷键体系、无障碍键盘闭环、移动端入口降级、与权限联动的命令,以及上线前 10 项检查表。

工具技巧设计体验前端工程
文档站首页截图(Starlight 搭建):导航、搜索框与内容分区
指南
产品的第二官网:vibe 项目的文档站搭建实战

AI 能糊出像文档的页面,但信息架构、版本化、搜索、与代码同步才是文档站能用的关键。本文给一人团队完整路线:README/帮助中心/文档站决策表、亲核过的四家工具对照(Fumadocs/Starlight/Docusaurus/Mintlify)、四块内容加两条读者路径的信息架构、快照式版本化、DocSearch 与本地搜索、OpenAPI 驱动的 API 参考、有帮助吗反馈闭环,以及上线前 10 项检查表。

开发工作流工具技巧增长与营销