这两段录播发布于 2023 年,技术实现、个人经历与观点均为当时情况。
在线文档看起来仍是一个网页,但让多人顺畅地编辑同一份内容,需要解决哪些普通业务页面不常面对的问题?谢文清把它拆成两条主线:编辑器怎样理解和呈现输入,协同系统怎样在并发修改之后得到一致结果。
他曾在不同规模的公司与创业团队工作,经历后端和前端开发,当时在飞书文档团队。这次分享也直接使用在线文档展开,呼应团队通过文档开会、讨论和分享的工作习惯。约 69 分钟技术讲解之后,还有约 24 分钟访谈,继续讨论 AI、个人成长与团队文化。技术分享 00:03–02:30
让文字可以编辑,只是第一步
输入框不等于富文本编辑器
input 和 textarea 能接收输入,却很难直接承担丰富的段落、格式、表格和复杂交互。谢文清先介绍浏览器提供的可编辑能力:designMode 可以让文档可编辑,而 contenteditable 可以作用于指定区域。
区域控制很重要。在线文档还有导航、评论、工具栏等内容,并不是整个页面都应该允许用户修改。早期实现可以通过独立文档或 iframe 划出编辑范围,也可以把可编辑属性放在需要处理的元素上。技术分享 02:31–04:45
再结合浏览器的编辑命令,可以快速实现加粗、斜体等基础功能。这样确实能够搭起一个简单编辑器,但当产品要求一致的结构、丰富能力与长期可扩展性时,浏览器默认行为就不再足够。技术分享 04:45–06:07
同一个动作,可能生成不同的 DOM
谢文清用回车和删除回车的例子说明问题。看起来相同的操作,在不同浏览器和版本中,可能产生不同的 div、br 或文本节点组合。用户认为自己已经回到最初状态,底层结构却未必完全恢复。
加粗也类似:视觉效果接近,不代表输出标签和结构相同。编辑器如果完全依赖这些结果,就需要不断处理兼容性差异;更复杂的功能还可能超出已有命令的能力范围。
因此,难点不是有没有一个 API 可以让文字变粗,而是怎样建立可以稳定理解、修改和保存的文档结构。技术分享 06:07–10:01
从浏览器行为,走向自己的数据模型
拆开输入、模型和渲染
分享把编辑器演进概括为 L0、L1、L2 三个阶段,用来讨论控制范围的变化,并非要求所有产品沿同一条路线升级。
第一个转变,是减少对浏览器编辑命令的依赖。编辑器继续利用部分输入、光标与选区能力,但把用户动作转换成自己的数据模型,再根据模型渲染。这样,文档的含义不再只由某次浏览器生成的 HTML 决定。
他将系统拆成输入、数据模型和自定义渲染三个部分。输入可以有不同程度的受控方式;模型可以采用字符串、数组或树;渲染则可以结合 DOM、SVG、Canvas 等技术。选哪一种,要看产品需要控制什么,以及愿意承担多少实现复杂度。技术分享 10:01–13:04
文档模型,是理解不同编辑器的重要入口
他随后比较了几类有代表性的设计。Changeset 用变更描述文档的演进,配合文本及属性信息表达内容;Quill 的 Delta 以操作序列描述插入、删除、保留和格式。变更不仅能表示一次编辑,也能从初始状态逐步构造完整文档。
Draft.js 采用分层组织的编辑状态与内容结构;Slate 则用节点树表达编辑器、元素和文本之间的关系。平坦结构与树形结构各有表达方式,也会影响后续修改和渲染如何组织。
谢文清还解释,成熟产品中同时出现多种模型,不一定是随意堆叠,可能与历史演进和兼容既有实现有关。理解当前接口时,也需要知道它从哪里来。技术分享 13:04–18:00
更强的控制,也意味着自己承担更多基础能力
下一阶段,编辑器进一步自行管理布局、光标和选区,以满足一致体验与高级功能。这里的“减少依赖”并不等于彻底停止使用浏览器输入能力:实现中仍可能保留隐藏的可编辑元素来接收输入,再自行决定怎样显示。
分享也提到从 DOM 走向 Canvas 的产品实践,但没有把渲染技术直接当作阶段划分的唯一标准。核心仍是哪些行为由浏览器决定,哪些由编辑器自己的模型与规则决定。技术分享 18:00–20:13
多人协同,需要同时解决传输和冲突
普通富文本编辑器主要处理一个使用者的输入。在线协作文档则要让不同用户及时收到彼此的操作,并在同时修改时处理冲突。谢文清把它概括为数据同步与冲突处理,两者缺一不可。
实时通道要有故障时的行为
通信层需要支持双向、实时的消息交换,还要维持稳定。多接入点、心跳和断线重连,是分享中提到的基础措施。如果长连接无法工作,也需要考虑通过其他请求方式兜底,尽量保持可用,只是实时性可能下降。
这些机制的价值不仅在正常演示里可见,更在网络变化、连接中断或服务异常时体现出来。用户正在写的内容,不能因为底层通道暂时变化就完全失去处理路径。技术分享 20:13–22:26
房间与通道,是资源组织与业务隔离的问题
通信层还要支持不同业务复用。分享介绍了协同实体与其内部通道的分层:一个文档或协作空间可以作为较大的组织单元,内部再承载头像、权限或其他消息类型。
为什么不把每个小功能都拆成独立实体?他的解释是,推送和资源管理本身也有成本。适当聚合与隔离,需要一起设计;他用进程与线程的关系帮助听众理解这种分层意图。技术分享 22:26–24:19
收到同样的操作,仍可能得到不同文档
分享用一个简单例子进入冲突问题:两位用户看到相同的初始内容,都在同一位置插入字符,各自在本地立即显示结果。若服务端只是把操作原样转发,另一人的插入到达之后,两边可能得到不同的字符顺序。
删除、格式修改和文档块也会遇到类似问题。冲突的关键不在消息有没有送达,而在并发操作基于怎样的状态、以什么规则执行。
谢文清把它联系到分布式系统的一致性问题。对于编辑体验,本地输入应及时响应;暂时离线或网络延迟又是现实条件。因此,系统可以允许短时间看到不同状态,但需要在操作传播和处理完成后收敛,而不是要求每次击键都等待所有人同步完成。技术分享 24:19–29:19
给编辑加锁,或者像文件合并那样让用户不断手动处理冲突,都不太符合实时协作文档的预期。分享因此转向 OT 与 CRDT 两类方法,解释它们怎样把冲突处理纳入算法与数据设计。技术分享 29:19–31:00
OT:先理解操作,再理解转换
三类基础操作可以组合成复杂编辑
在讲解的文本模型中,一次编辑可以拆成插入、删除和保留。保留表示跳过已有的一段内容,继续在后面处理;替换则可以拆成删除与插入。
这样的操作序列描述了怎样从一个文档版本走到下一个版本。它与只传递最终文本不同:系统保留了修改动作,才有条件分析并发修改之间的关系。技术分享 31:00–32:49
转换的目标,是让不同路径到达相同结果
假设 A 与 B 都基于同一个文档版本 S。先执行 A 后,原来的 B 可能需要变成 B′;先执行 B 后,A 也可能需要变成 A′。转换希望满足下面的关系:
apply(apply(S, A), B′) = apply(apply(S, B), A′)
谢文清用菱形图说明这一点:起点相同,两条路径经过各自的操作与转换,最终汇合。公式是要满足的目标,真正的工作仍在转换函数怎样处理各种组合。技术分享 32:49–35:04
同时插入时,还需要确定的处理顺序
在源码讲解中,他把三种基础操作的组合拆开,观察转换函数的分支。某些分支可以合并处理,但不能因此忽略顺序:当两边同时在同一位置插入时,采用哪一方优先的约定,会影响最终结果。
只让每个客户端按照“先自己的,再别人的”处理,未必能形成相同结果。这里引出他重点分析的客户端—服务端模型:不仅需要转换函数,还需要围绕版本、操作次序和确认消息建立完整协议。技术分享 35:04–40:00
服务端与客户端,要以一致的上下文处理操作
服务端维护操作历史与版本
客户端提交操作时携带版本信息。服务端根据它所依据的版本,判断当前操作是否需要与之后已经确认的操作进行转换,再将处理后的操作纳入历史。
来源客户端收到确认,其他客户端收到广播。广播出去的可能已经是转换后的操作,因此不能把服务端理解为不参与判断的纯转发通道。技术分享 40:00–42:12
客户端必须区分“已同步”和“还在等待”
谢文清以 ot.js 的三个状态展开:已同步、等待确认,以及等待确认时又产生了本地缓冲。用户不会因为一个操作还在路上就停止输入,因此后续编辑需要保存下来。
处于已同步状态时,收到远端操作可以直接应用;如果本地还有待确认操作,就需要处理它与远端操作的关系;如果还有缓冲,又要把后续编辑纳入转换。收到自己的确认,也不意味着把同一修改再执行一次,而是推进状态并按需发送缓冲内容。这些状态和转换路径可以在 ot.js 客户端源码中对照。技术分享 42:12–47:05
演示把消息顺序与本地状态放在一起看
现场用两个客户端和一个服务端演示并发编辑。先到达的操作被接受并传播;后到达的操作根据已有历史转换。另一端收到远端操作时,还要结合自己是否正在等待确认,决定如何应用和更新状态。
这段演示的价值,是把公式里的 A、B、A′、B′放回实际消息过程。某个函数单独满足一种代数关系,还不足以说明整个系统正确;版本判断、确认、缓冲与传播也必须遵循相应的约定。技术分享 47:05–49:56
从文本走向文档块,操作种类会增加
分享比较了 ot.js、Etherpad 的协同实现、Delta 与 JSON 类型的 OT。它们的模型和接口名称有所不同,但都需要表达操作、处理并发并与客户端状态配合。
JSON 类型的操作不仅面对一串文字,还要表达路径及其上的对象、数组或数值变化。文本子类型可以进一步使用专门的规则,文档块则由更通用的结构组织。谢文清借此解释复杂文档为什么可能组合多种实现,而不是只使用一个文本操作函数。技术分享 49:56–54:00
随着操作类型增加,需要考虑的组合也会增长。表格、块结构与更复杂业务会提高转换规则的实现和验证成本;在他分析的中心化模型里,服务端的计算与网络也影响并发规模。具体人数上限属于产品和部署条件,不能仅凭使用 OT 就推出一个统一数字。技术分享 54:00–55:11
CRDT:通过数据与合并规则处理并发
合并规则要面对重复和乱序
进入 CRDT 部分,谢文清把关注点转到数据类型的设计。网络可能重复发送消息,消息抵达次序也不一定等于发送次序;幂等、交换和结合等性质,有助于理解某些合并规则为什么能应对这些情况。
他用计数与增减量的例子说明,发送完整结果与发送变化量会带来不同问题。即使加法可以交换顺序,重复应用同一增量仍可能出错,因此还需要明确识别和处理重复。不同 CRDT 类型的前提各不相同,不能只列出几条数学性质,就跳过完整的实现条件和正确性分析。技术分享 55:11–58:57
YATA 的讲解关注位置标识与确定顺序
随后,分享介绍面向协同文本的 YATA 思路。字符或插入项不只携带内容,还需要标识与相邻关系。不同用户在接近位置插入时,系统根据约定的顺序规则处理,避免单纯依赖某个瞬间的数组下标。
逻辑删除也保留了供协同处理所需的信息,之后再考虑回收。谢文清结合图示讨论插入位置、冲突集合和排序规则,同时说明现场没有展开全部证明。这里的重点,是理解位置与标识为什么需要进入模型,而不是把简短演示当成已经覆盖所有边界的实现教程。技术分享 58:57–64:17
后写优先,同样需要定义怎样比较先后
他还提到 Last Write Wins 一类处理方式,以后来的写入覆盖之前的值。这样的规则看起来直观,但系统仍需要定义可比较的顺序,并处理相同时间或不同节点视角下的差异。
这也呼应前面 OT 的讨论:仅仅知道策略名称,不足以说明并发系统会得到什么结果;需要进一步看标识、时间和消息处理怎样配合。技术分享 64:17–65:14
技术选择,还要看内存、部署与产品要求
在对比环节,谢文清更关注当时产品中的现实成本。更丰富的标识和历史信息会占用空间,逻辑删除与回收需要设计;复杂文档又对内存、响应和体验提出要求。
他对“CRDT 是否一定代表协同未来”保留疑问,也表达了企业数据管理方面的顾虑。这是他的场景判断,不能进一步概括为 CRDT 天然不适合文档,或者只能在没有服务器的网络中使用。Yjs 官方文档就提供了不同网络与持久化方式的组合;安全和权限还取决于具体部署。
分享结束时,他强调编辑与协同也只是在线文档复杂性的一部分。理解每种设计为何出现、怎样处理真实约束,比只记住某个算法名称更有价值。技术分享 65:14–68:43
AI 会怎样进入前端研发流程
访谈的第一个问题回到当时迅速受到关注的 ChatGPT。谢文清从研发流程与软件架构两个维度分析,没有把它仅仅看成代码补全工具。
在需求阶段,AI 可能帮助解释和拆解需求,甚至将其整理为更一致的行为用例。他以 BDD 的沟通方式为例,认为产品、研发与测试要共享同一种描述,本身有学习成本;工具有机会降低这种转换成本。
设计、开发、测试与发布,也都有可以尝试的辅助环节。现场提到的产品和能力属于 2023 年语境,更重要的是这套观察方式:沿着实际交付流程寻找问题,而不是先假设某一个工具能包办所有工作。访谈 00:03–03:31
工程师需要更清楚地定义边界与接口
他把当时的 AI 定位为副驾驶。工程师描述场景、目标和约束,让工具参与实现,再检查结果。工作由直接写代码,扩展为问题拆解、明确表达、生成和评审。
模块之间的关系、输入输出与协议,在这里尤其重要。只有自己先想清楚,才更容易把任务交给工具。AI 是否减少低级错误仍要实际验证,但这种工作方式会让抽象、架构与评审能力变得更值得重视。访谈 03:31–06:11
“提示层”是设想,完整交付仍然需要人负责
谢文清还设想,未来软件架构中可能出现需要持续维护的提示层,让一部分生成行为围绕稳定的意图描述组织。主持人从组织成本和人员分工出发,讨论工具可能怎样改变工作。
两人都强调学习使用新工具,同时也承认把全部工作无条件交给 AI 并不现实。他们还提到低代码与流程化工作的结合空间。这些属于当时的探索和判断,不能倒写成已经在团队全面落地的架构。访谈 06:11–10:57
前端成长:持续学习、技术积累与业务理解
深度和广度,需要不同的投入方式
谢文清把持续学习放在最前面。无论处于什么级别、是否转向管理,都需要维持理解新问题的能力。
技术深度意味着在某个方向形成自己的分析;广度则帮助看见边界条件、关联模块和其他领域的经验。他建议对知识进行分类,区分需要深入研究的主线与需要建立基本认识的方向,避免把“知识很多”变成对所有主题平均用力。访谈 10:57–13:45
业务不是技术成长的对立面
面对每天做业务需求、似乎没有时间提升技术的感受,他提醒,深入理解业务本身就是能力。相同需求由不同工程师处理,架构、边界、质量与后续维护可能差别很大。
他的成长方式,是从业务中发现值得深入的技术问题,再把它做透。站在团队或产品目标上看,技术是否解决了真实问题,也是评价工作的关键。访谈 13:45–15:35
主持人随后补充自己的管理经历:工作重心改变之后,仍会学习语言、国际化协作和管理知识;即使不再每天写代码,也希望保留学习技术与解决问题的能力。两人都反对把管理理解为不再需要技术判断,也不赞成因为业务表面上像是在写界面,就低估其中的工程差异。访谈 15:35–18:45
团队文化,要落实到日常怎样做事
最后的问题是“字节范”。谢文清将它理解为共同做事的方式,并按六个方面介绍自己的感受:始终创业、多元兼容、坦诚清晰、求真务实、敢为极致和共同成长。名称可在字节跳动公开文化说明中核对。
他所理解的创业心态,既包含投入,也包含承认尝试可能失败;坦诚清晰则体现在表达真实想法、就事论事和较直接的沟通。对他印象较深的,是可以带着问题或想法,与不同层级的同事交流。
高标准也不只是把需求上线,而是考虑质量、边界以及更好的实现。共同成长则再次回到持续学习,希望人在完成工作时也积累长期能力。
他最后保留了一个区别:文化倡导与每个人、每个团队的实际落实之间可能有距离,个人感受不能代表所有工作场景。访谈因此没有停留在口号,而是回到具体的表达、协作和交付行为。访谈 18:45–24:01
