一次失败面试成了起点
Roleframe 的诞生,源自一场没有通过的面试。作者经历了漫长的求职后,终于拿到一次系统设计面试,题目是他从未接触过的方向:设计一个文档编辑器。他尽力作答,最终没有拿到录用通知,但这个问题一直留在他脑子里。
于是他决定认真学透这件事。编辑器的每一次改动如何被记录,布局引擎如何决定元素的位置,专业工具底层到底怎么搭建,他都从头研究。学到一半时,他回头去看自己求职期间用过的那些简历生成器,才真正看清它们是什么。
大多数简历生成器只是一个表单
剥掉营销包装,几乎所有简历生成器都是同一种结构:一个数据模型加一个模板渲染器。左边是固定字段,右边是静态预览,还有一堆规则限制简历能长成什么样。想要一个数据模型里没有的栏目,不行;想让两个条目并排显示,也由不得你。
这种形态不是偶然。表单是那种一个周末就能搭出来的部分,而真正的编辑器——能拖拽栏目、控制布局、最后还能输出干净多页 PDF——才是难啃的骨头。他用过的每一个生成器都绕开了这个难点。因为一次失败面试而把编辑器从里到外学了一遍之后,他手里正好握着别人跳过的那部分知识。于是 Roleframe 就做成了这样:一个真正的拖拽式简历编辑器,AI 定制功能只是叠加在上面,而不是取代编辑器本身。
状态用映射而不是列表来渲染
技术栈方面,项目使用 Next.js 的 App Router、React、TypeScript、Tailwind,数据库是 Postgres 并直接写原生 SQL,不用 ORM,后台任务跑在任务执行器上,还从零写了一个自定义事件溯源引擎。
事件日志是唯一事实来源,但 React 需要的是当前值,所以每次追加事件都会物化出一个当前状态。值得说的是,这个物化后的结构是专门为渲染设计的,而渲染想要的东西和存储完全不同。
模型里几乎没有数组。块(blocks)是按块 ID 索引的对象,覆盖层(overlays)是按覆盖层 ID 索引的对象,页面(pages)是按页面 ID 索引的对象,页面内部的布局节点也是按节点 ID 索引的对象。整个模型里唯一的有序列表,是挂在布局节点下的子节点 ID 列表——顺序只出现在“内容按什么顺序出现”这一个地方,别处都不该有。
布局节点只有两种:一种是容器,持有子节点 ID 数组;另一种是块引用,通过块 ID 指向内容。树只负责指向内容,从不直接持有内容。
这种结构带来三个直接好处
组件只订阅一个 ID。一个块通过自己的 ID 选中对应实体,拿到的是稳定引用,不会因为别处变化而跟着重渲染。编辑操作只影响相关节点,不会波及整棵文档树。
从一次失败面试,到一个真正可拖拽的简历编辑器,Roleframe 把别人绕开的硬骨头当成了核心功能来做。
热门跟贴