02判断5 min

每次我从零手写一个东西,最后总会发生同样的事

我手写过四次编辑器。三次最后都用库重写了。第四次连两天都没撑到。

作者 Salva Castells

封面图: Globetrotter19, CC BY-SA 4.0.
本文目录

2024 年 9 月,我花了三周做一个块编辑器。想法还是老一套:每个段落是一个块,可以拖动、可以重排,每个块还有自己的 AI 按钮用来重写。没什么新鲜的。Notion 做这件事已经很多年了。

我用 dnd-kit 做拖拽,每个块一个编辑器实例。Block 组件接收 id、index、content、onDelete、onContentChange,一个带五个参数的 regenerateContent 函数,整份块列表用来提供上下文,还有一个放在 ref 里的 AbortController 用来中断生成。它能跑。那一周有个提交的名字就叫 "never give up!!"。这就是留在记录里的东西。

六周之后,这些全被删掉了,取而代之的是一个带扩展的单一编辑器。

当你自己编排块的时候,会坏在哪里

问题不在拖拽。拖拽做得挺好。问题是其他所有东西,而且我是按这个顺序一个个发现的:

  • 把内容同步进去。 每个块都有一个 useEffect,比较 content !== editor.getHTML() 来决定要不要调 setContent。用字符串比较 HTML 来判断状态有没有变,正是那种看起来能用、直到不能用为止的做法。
  • 跨块选择。 根本不存在。如果每个块都是自己的编辑器,用户就无法从一个段落的中间选到下一个段落的中间。没有局部修补的可能:要么是一份文档,要么是 N 份文档。
  • 撤销。 每个实例都有自己的历史栈。Ctrl+Z 撤销的是光标所在块里的操作,而不是你最后做的那件事。没人会预期这个。
  • 粘贴。 你把三个段落粘进一个块,它们就都待在那一个块里。要拆开它们,就是手工重新实现文档 schema 本来就会做的事。

这四件事,开始时我脑子里一件都没有。它们出现的顺序,正是我写下来的顺序。

重写比原型还短

10 月中旬,block/ 目录旁边已经并存着一个 block2/ 目录,里面只有三个文件:编辑器、工具栏和一个 hook。整个 hook 只有四十行:StarterKit、TextStyle、FontFamily、TextAlign、Highlight、Placeholder。数组里声明六个扩展,统一的历史记录、连续选择、带 schema 的粘贴就都有了。11 月又加了任务列表、代码块和按 URL 插图:每个都是一个 import 加数组里的一项。

目录名说明了一切。block2 不是重构。它是承认旁边那个已经救不回来了。

这个网站的 CSS 也一样。我曾有一个一千六百八十一行、全是自定义类的文件。改成组件之后,唯一活下来的是颜色 token:globals.css 里的一百一十三行。其余都是重新造轮子。

库永远占优的地方

这不是"用库就对了"。而是有一些具体的类别,问题比从外面看起来大得多,而且永远是同一批:

  • 富文本编辑器。 选择、历史、粘贴和 schema 是同一个系统。你不能四选三。
  • 认证和会话。 没人会感谢你亲手写的 refresh token。
  • 日期和时区。 那个诡异的边界情况会在生产环境找上你,而不是在你的机器上。
  • 样式。 自建的类系统一开始更干净,最后会变成一个文档更差的框架。

共同的信号是这个:如果难点不是第一屏,而是那些开始时看不见的部件之间的交互,那库赢。如果难点是你的业务逻辑,那就没有库可用。

我现在怎么决定

我把顺序反过来了。现在我从库开始,只有在它挡住我的时候才离开。

去年夏天我需要一个协作编辑器,就是这么解决的:从 PlateJS 的模板起步,上面加 Yjs。第一个提交之后三天,屏幕上就已经有其他用户的远程光标了。要是几个月前,我会从带五个 props 的 Block 开始。去年做一个 Markdown 笔记工具时,我连编辑器都没考虑过:CodeMirror,CSS 里 import 一下 highlight.js,然后干自己的事。

还有一种情况更快。2025 年 4 月我写了一个带工具的 agent,一个带上下文工具的 agent.ts。第二天就有一个提交把它删掉,直接用供应商的流式接口。从构建到删除不到二十四小时,而这是个好结果。

真正的代价

手写并不是浪费时间:我知道编辑器里粘贴是怎么回事,因为我曾经把它实现错过。这种知识不是读文档能买到的。

代价在于你什么时候发现。三周手写块,加上 block/ 和 block2/ 并存的六周,就是两个月。一个 3D 地图的第一版页面撑了十天,之后一个叫 map-v2 的分支把它整个替换掉;那里的代价是十天,不是两个月,因为我更早收手了。

两者的差别不在于我开始时懂多少。而在于我花了多久才接受:我懂的已经够了。

手写是一种昂贵的读文档方式。有时值得付这笔钱;不值得的是为同一件事付两次。