简历回答逐字稿:低代码变量编辑器与数据总线
关联:简历追问:低代码变量编辑器与数据总线、15分钟:公式编辑器。
30 秒开场
变量编辑器这块我主要解决的是低代码流程里的跨节点参数传递。用户不应该手写一堆脆弱字符串,比如 ${节点名称.字段名},因为节点改名、复制、删除以后很容易坏。
所以我用 Tiptap/ProseMirror 做了结构化变量编辑器。用户看到的是一个变量胶囊,底层存的是稳定的 nodeId、字段 path 和展示名。运行时再通过表达式引擎和 ExecutionContext 解析这些引用,把上游节点输出传给下游。
如果面试官问:为什么不用普通 textarea?
我会说 textarea 做起来快,但它只能存字符串,无法保证变量引用的结构稳定。
比如用户引用“表单节点 A 的 customerId”。如果只是字符串,A 改名了、字段展示名改了,公式可能就坏了。用 ProseMirror 自定义 node 后,变量在文档里是一个原子节点:展示名可以变,但底层仍然是 nodeId + path。
另外,原子节点可以控制用户不能把变量删一半、改一半,这对低代码配置很重要。否则表面看是公式,实际运行时已经变成不可解析字符串。
如果面试官追:原子化胶囊怎么防止被破坏?
我会说主要靠 ProseMirror NodeSpec 和交互拦截。
变量节点设置成 atom 或类似不可编辑节点,用户可以整体选中、整体删除,但不能把光标插到中间。对 Backspace、Delete、方向键、复制粘贴、undo/redo 这些边界要单独处理。粘贴时也不能只粘贴展示文本,而要保留变量节点的结构数据;如果跨文档粘贴无法识别,就降级成普通文本或提示引用失效。
如果面试官追:${} 和 / 菜单怎么实现?
我会这样回答:
输入 / 或 ${ 时,根据当前 selection 和上下文触发 suggestion。候选项来自变量作用域:全局变量、当前流程变量、上游节点输出。这里不会把所有节点都列出来,而是根据 DAG 关系过滤,只允许引用当前节点之前可达的上游输出。
提示框定位我会用 Tiptap/ProseMirror 的 view.coordsAtPos(state.selection.from) 拿到光标在 viewport 里的坐标,再用 Portal 挂到 body,必要时用 floating-ui 做边界翻转。这样在弹窗、滚动容器里也不容易错位。
如果面试官追:表达式引擎怎么做,为什么不用 eval?
我会说公式本质是用户输入的逻辑,不能直接交给 JS 执行。eval 或 new Function 会带来全局对象访问、原型链污染、XSS 等风险。
所以我会先做 Lexer,把字符串拆成 token;再 Parser 生成 AST;最后 Evaluator 在受控上下文里解释执行。上下文里只有我注入的变量和白名单函数,比如数学函数、日期函数、空值处理函数。用户不能访问 window、document、Function、prototype 这些入口。
同时 AST 让我们能在输入阶段就提示括号不匹配、函数参数数量不对、未知字段这些错误,而不是等运行时才报错。
如果面试官追:跨节点数据总线怎么保证不串?
我会说每次流程执行都有一个 ExecutionContext,按 nodeId 隔离存储节点输出:
context.outputs[nodeId] = {
status: 'SUCCESS',
data: {...}
}下游节点执行前,变量解析器读取配置里的变量节点,按 nodeId + path 去 context 取值。并行节点不会共享可变局部变量,而是都写到自己的 nodeId 下。这样可以避免 A 节点输出被 B 节点覆盖。
对于未命中分支的变量,最好在发布校验或运行时明确报错,不能默默解析成空字符串。否则问题很难排查。
如果面试官追:异步函数怎么办?
我会说第一版公式引擎尽量保持同步,因为同步表达式更可控。如果未来需要 GET_USER_INFO(id) 这类异步函数,我会先做静态分析,判断 AST 里是否包含 async function。没有就走同步 evaluator;有才进入 async evaluator。
同时异步函数要做批量和缓存,比如 DataLoader 模式,把短时间内的多个 id 查询合并,避免每行公式都打一次接口。
收尾句
所以这块我不是把变量做成字符串替换,而是把变量引用、公式解析、执行上下文和节点输出都结构化。这样低代码流程的参数传递才可维护、可提示、可校验。