POST://rich-text-editor-selection
带点橘子味的馒头的头像
带点橘子味的馒头

FRONTEND / DESKTOP DEV

返回文章列表

富文本编辑器选型指南:从 contenteditable 到 TipTap

1 分钟· 660 68 次阅读

做博客后台时被问到最多的一个问题是:"编辑器用哪个?"这篇把主流方案摆开讲清楚,并给出我选 TipTap 的理由。

先泼冷水:别从零写

contenteditable 看起来简单——给 div 加个属性就能打字。但真要做出可用产品,要处理的问题多到劝退:

  • 光标位置管理(跨浏览器不一致)
  • 撤销/重做栈
  • 剪贴板格式清理
  • 输入法(IME)兼容
  • 嵌套列表、表格、图片的 DOM 结构

结论:除非是教学实验,否则不要从零写编辑器。

主流方案对比

方案 定位 上手难度 定制能力 生态
contenteditable 原生 极高(都要自己做)
Quill 经典富文本 成熟但慢
TipTap 无头编辑器 高(ProseMirror 内核) 活跃
Lexical Meta 出品 中高 较新

为什么选 TipTap

无头(headless) 是关键——它只管编辑逻辑和结构,不管样式。这意味着:

  1. 样式完全跟着项目设计系统走,不会"编辑器是编辑器,网站是网站";
  2. 基于 ProseMirror 的稳定内核,光标、撤销、IME 这些地狱级问题已经解决;
  3. 扩展机制清晰,一个扩展就是一个包:
import { useEditor, EditorContent } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import Image from "@tiptap/extension-image";
import Placeholder from "@tiptap/extension-placeholder";

const editor = useEditor({
  extensions: [
    StarterKit,                 // 加粗/斜体/列表/代码块等基础
    Image,                      // 图片
    Placeholder.configure({ placeholder: "开始写点什么吧…" }),
  ],
  immediatelyRender: false,     // SSR 必设
});

自写扩展:给编辑器加"上传图片"

TipTap 的扩展自定义能力体现在这里——工具栏加按钮,接上传接口:

async function addImage() {
  // 用户选文件 → 上传 → 拿到 URL → 插入编辑器
  const fd = new FormData();
  fd.append("file", file);
  const res = await fetch("/api/upload", { method: "POST", body: fd });
  const { url } = await res.json();
  editor.chain().focus().setImage({ src: url }).run();
}

与后端存储的解耦

编辑器产出 HTML,但内容库可以是 Markdown。用 turndown 做单向转换:

import TurndownService from "turndown";

const td = new TurndownService({ headingStyle: "atx" });
const markdown = td.turndown(editor.getHTML());

这样编辑器只是"可视化外壳",数据永远是最可移植的 Markdown。

三个常见坑

解法
SSR 报 hydration 错 immediatelyRender: false
图片 URL 存了绝对地址 存相对路径,渲染统一拼
工具栏按钮没响应 onMouseDownpreventDefault,否则编辑器失焦

小结

选型建议一句话:

  • 快速交付 + 样式无要求 → Quill;
  • 深度定制 + 设计系统统一 → TipTap
  • 团队想深度掌控内核 → Lexical;
  • 千万别从 contenteditable 裸写。

编辑器是"看起来简单、做起来地狱"的典型——站在 ProseMirror 肩膀上,才是性价比最高的选择。

相关文章