用 TipTap 做富文本编辑器的经验谈
约 1 分钟· 415 字 44 次阅读
后台编辑器选了 TipTap(基于 ProseMirror),这块踩了不少坑,整理成文。
为什么选 TipTap
- 无头(headless),样式完全自己控制;
- 扩展机制清晰,
StarterKit开箱即用; - React 绑定成熟,
useEditor+EditorContent几分钟就能跑起来。
接入图片上传
TipTap 官方有 Image 扩展,但默认只支持传入 URL。要支持本地上传,需要自己在工具栏按钮里做文件上传:
import Image from "@tiptap/extension-image";
const editor = useEditor({
extensions: [StarterKit, Image],
});
async function uploadImage(file: File) {
const fd = new FormData();
fd.append("file", file);
const res = await fetch("/api/admin/upload", { method: "POST", body: fd });
const { url } = await res.json();
editor.chain().focus().setImage({ src: url }).run();
}

上传接口负责:校验文件类型(png/jpeg/webp/gif/svg/avif)、限制 5MB、生成随机文件名防止冲突。
HTML 与 Markdown 的转换
编辑器产出 HTML,但博客内容想用 Markdown 存储(可版本管理、可迁移)。用 turndown 做单向转换:
import TurndownService from "turndown";
const td = new TurndownService({
headingStyle: "atx",
codeBlockStyle: "fenced",
});
const markdown = td.turndown(editor.getHTML());
存储 Markdown、渲染时再用 remark 转回 HTML,等于把编辑器当成"Markdown 的可视化外壳"。
几个值得注意的坑
immediatelyRender: false:Next.js SSR 下必须设置,否则 hydration 报错;- 图片路径:上传后返回相对路径(如
/uploads/202608/xxx.png),存库和渲染都一致,别存绝对地址; - 占位符:
Placeholder扩展的配置要在StarterKit之后,否则可能不生效。
小结
TipTap 的可定制性很强,代价是很多细节要自己接(上传、校验、转换)。但一旦跑通,后台编辑体验和前台展示能完全统一,值得投入。