下一代低代码渲染框架 nop-chaos-flux 的设计原则

引言

nop-chaos-flux 是 Nop 平台的渲染层------一个基于声明式 DSL 驱动的低代码运行时,用于在浏览器中渲染和执行由 Schema 描述的应用页面。它的 DSL 表面形态与百度 AMIS 相似,但进行了概念统一化(如消除 xxxOn 后缀字段族);内部的编译模型、运行时架构和表达式引擎均为重新设计。

大多数低代码平台把 Schema 当作运行时的输入配置。运行时同时背负编辑态结构、领域语义和调度逻辑------每新增一种能力,就多一组原语、一个全局 provider、一条 schema 权威通道,核心持续膨胀。

Flux 从一条不同的路径出发:把 DSL 从运行时输入格式提升为独立的一等结构层。

1. DSL 优先

DSL 不是给运行时喂的输入格式,而是平台的一级制品:一个独立可操作的结构层。在进入运行时之前,它就已经拥有自己的生命周期、变换空间和组织规则。

很多系统也有 DSL,但 DSL 只是运行时的输入格式,不存在独立的结构操作。Flux 不这样看。Flux 的 DSL 在运行时之外就已经是可编辑、可组合、可裁剪、可变换的结构层:

操作 含义
编辑 源码位置保留、别名、编辑器元数据、round-trip 保真
合并/继承 x:extends 式继承、覆写展开、片段组合
裁剪 权限裁剪、feature flag 裁剪、profile 组装
变换 i18n 字符串替换、静态默认展开
元编程 通过结构约定(而非运行时接口增长)表达变异

DSL 变换是分层的:权限裁剪、i18n 替换、默认展开各自独立运作。移除编写态元数据不得改变运行时行为。


2. 编写-执行分离

即使选择了 DSL 优先,很多系统仍然只维护一种模型,让运行时直接背负编辑态结构。Flux 刻意不这样做,而是把 Authoring Model 与 Execution Model 放在预编译边界两侧。

这条原则的重点不是"有两套模型"本身,而是承认两侧有不同的优化目标:编写态服务于理解、编辑、组合与保真;执行态服务于简化概念、降低运行时负担、稳定执行语义。

双向优化目标

维度 编写模型 执行模型
优化目标 可理解性、领域表达力、编辑保真 性能、内部概念统一、运行时开销最小化
结构形态 保留源码位置、别名、编辑器元数据、领域编辑结构 已组装的 Final Execution Schema,无冗余
正确性标准 round-trip 保真、作者意图不丢失 行为等价、执行确定性
可替换性 多种编辑器/设计器/协作引擎可产出同一 DSL 同一 Final Execution Schema 可在不同运行时宿主执行

边界意义

预编译边界的意义,不只是"提前做一点优化",而是把本来不该落在运行时表面的结构问题留在结构层解决:

  1. 编译期结构决策 --- type 解析、renderer 绑定、默认展开在 loader 阶段完成,运行时零开销。
  2. 编译期策略裁剪 --- 权限节点、feature flag 分支在进入运行时之前已删除,运行时根本看不到。
  3. 编译期 action DAG 组装 --- then/onError/parallel 编译时组装为无环执行图,运行时不需要图发现或环检测。
  4. 统一 Value IR --- Value 的所有形式(literal/expression/template/array/object)编译为统一 IR,运行时统一求值。

如果一个问题能在结构变换层解决,就绝不拖进运行时表面。


3. 响应式数据驱动

Flux 的核心执行模型是响应式的,而且是声明式的。作者不需要显式搭建命令式联动过程------只要一个动态值通过 field path、name 或 ${expr} 读取了 scope 中的路径,它就自动落入依赖图,依赖变化时被重新求值。

基本节奏:求值 → 收集依赖 → 变更传播 → 定点重求值/失效 → 重新发布

依赖跟踪是 Value 原语的内建设计语义。依赖在求值时自动收集,不是事先静态声明。Value、Resource、Reaction 都使用这一机制,但命中后果不同:Value 重新求值,Resource 标脏刷新,Reaction 可能触发 Capability。

当前实现通过 React 和 useSyncExternalStore 完成渲染宿主衔接,但原则本身不绑定 UI 框架。

读写与效果分离

  • :Value / Resource 发布值 / Host Projection 快照,全部通过 ScopeRef 只读访问。
  • 值写入 :用户编辑、FormRuntime.setValueScopeRef.update、Resource 发布等修改 owner-owned data 的路径属于数据 owner 侧,会发布 store/scope change,但不等同于 action 触发。
  • 命令 / 效果 :schema-authored command(API 调用、setValue action、提交、导航、宿主命令等)只通过 Capability 派发。
  • 变化 → 效果:数据变化不直接触发 action,中间必须经过 Reaction 或 Semantic Lifecycle Entry。

渲染宿主衔接

Store 层自洽运行响应式逻辑,React 只是订阅 Store 快照的渲染宿主。

  • Settled Update Turn 是 runtime-store 概念,不是 React useEffect 排序概念。
  • React concurrent mode 可以中断、重播、丢弃渲染;Flux 不约束这种调度行为,只定义 store 何时结算一轮更新、何时发布稳定快照。
  • 渲染宿主消费的是发布后的结果,不直接持有响应式协议对象。

4. 渐进式演化

Flux 的复杂能力不应靠不断发明新 primitive 获得,而应沿着既有简单形式自然生长。这条原则同时约束两件事:

  • 作者可见的 DSL 从简单形式自然扩展到复杂形式,不频繁切换心智模型。
  • 运行时内部的复杂能力优先从既有原语组合出来(派生系统),而非一遇到压力就扩原语集。

DSL 层演化:简单形式自然生长

概念 简单形式 复杂形式
literal → expression → anonymous source named data-source(Resource)
动作 单步派发 when 守卫 → then/onError 分支 → parallel 扇出 → 可编译为 DAG 级执行图
结构 visible(显示级) when(生命周期激活) → loop(集合展开) → dynamic-renderer(远程装配)
宿主写入 语义命令 通用 patch 式 applyPatch

值演化

同一个属性按需求复杂度选择对应形式。消费者端读值方式不变:${countries} 从 literal 到 data-source 始终统一。

jsonc 复制代码
// literal
{ "options": ["draft", "published", "archived"] }

// expression
{ "options": "${role === 'admin' ? adminOptions : userOptions}" }

// source:字段级匿名请求,不发布到 scope
{ "options": {
  "type": "source",
  "action": "ajax",
  "args": { "url": "/api/countries", "params": { "region": "${form.region}" } }
}}

// data-source:命名 Resource,带生产者生命周期和调度策略
{ "type": "data-source", "name": "countries",
  "action": "ajax",
  "args": { "url": "/api/countries" },
  "interval": 3000,
  "stopWhen": "${countries.complete}" }

动作演化

编译器将嵌套 schema 递归组装为 CompiledActionNode DAG(flux-compiler/action-compiler.ts),运行时直接遍历边执行,无需图发现或环检测。

jsonc 复制代码
// 单步派发
{ "action": "setValue", "args": { "path": "name", "value": "test" } }

// when 守卫:条件不满足时跳过,结果标记 skipped
{ "action": "setValue", "when": "${isEnabled}",
  "args": { "path": "name", "value": "test" } }

// then/onError 分支:按 ActionResult 三分类走不同路径
{ "action": "ajax", "args": { "url": "/api/users" },
  "then":     { "action": "showToast", "args": { "message": "保存成功" } },
  "onError":  { "action": "showToast", "args": { "message": "${error.message}" } } }

// parallel 扇出 + onSettled 聚合
{ "action": "parallel",
  "parallel": [
    { "action": "ajax", "args": { "url": "/api/notify/email" } },
    { "action": "ajax", "args": { "url": "/api/notify/sms" } }
  ],
  "onSettled": { "action": "showToast", "args": { "message": "通知完成" } } }

// 表单提交:submitAction 由表单节点拥有,按钮只是 component:submit 的薄触发器
{ "type": "form", "id": "profile-form",
  "submitAction": {
    "action": "ajax", "args": { "url": "/api/profile", "method": "post" } },
  "onSubmitSuccess": { "action": "closeSurface" },
  "onSubmitError":   { "action": "showToast", "args": { "message": "${error.message}" } } }

分支上下文(result/error/prevResult)在调度时自动注入求值环境(flux-action-core/action-core.ts createBranchEvaluationBindings)。

结构演化

visiblewhen 不是同义词:visible 隐藏的字段仍参与验证;when=false 的子树整体不激活、不参与生命周期。

jsonc 复制代码
// visible:显示级切换,节点仍存在
{ "type": "input-text", "name": "adminCode", "visible": "${role === 'admin'}" }

// when:生命周期激活,影响存在性和子树验证
{ "type": "fragment", "when": "${showSummary}",
  "body": [{ "type": "text", "text": "摘要内容" }] }

// loop:集合展开,每次迭代获得独立的 repeated-item scope
{ "type": "loop", "items": "${users}", "itemName": "user", "indexName": "idx",
  "body": [{ "type": "text", "text": "${idx + 1}. ${user.name}" }],
  "empty": [{ "type": "text", "text": "暂无数据" }] }

// dynamic-renderer:运行时远程装配,决定渲染什么片段
// 注意:它不是第二个 Resource 面------data-source 生产命名值,dynamic-renderer 装配片段
{ "type": "dynamic-renderer",
  "loadAction": { "action": "ajax", "args": { "url": "/api/schema/${componentType}" } },
  "body": { "type": "text", "text": "Loading..." } }

5. 词法所有权

这条原则是原则 3 的组织约束。数据、能力、资源、反应以及运行时边车跟随词法作用域或子树边界归属,不靠全局运行时大对象。

三种解析机制

数据查找(ScopeRef)、行为查找(ActionScope)、实例定位(ComponentHandleRegistry)是架构上分离的三种解析机制,各有独立的作用域规则。

词法遮蔽

子作用域通过自然词法遮蔽覆盖父级发布,而非全局覆盖。同名绑定在不同词法作用域中可独立存在:

jsonc 复制代码
// 页面 scope 有 items
{
  "type": "page",
  "data": { "items": ["a", "b"] },
  "body": [
    // dialog 子 scope 也有 items,遮蔽父级
    {
      "type": "dialog",
      "data": { "items": ["x", "y"] },
      "body": [{ "type": "text", "text": "${items}" }],
    }, // → ["x", "y"]
  ],
}

Resource 发布权

同一拥有作用域内,同一 binding target 不应被两个同时活跃的发布型生产者长期共同占有。这里约束的是 authoritative publication(Resource 的持续发布),不是普通写入:

jsonc 复制代码
// 合规:两个 form 在不同时间写入同一路径
{ "type": "dialog", "body": [
  { "type": "form", "id": "createForm",
    "onSubmitSuccess": { "action": "setValue", "args": { "path": "result", "value": "${result}" } } },
  { "type": "form", "id": "editForm",
    "onSubmitSuccess": { "action": "setValue", "args": { "path": "result", "value": "${result}" } } }
]}

// 违规:两个 data-source 同时宣称负责发布 "status"
{ "type": "page", "body": [
  { "type": "data-source", "name": "status", "action": "ajax", "args": { "url": "/api/a" } },
  { "type": "data-source", "name": "status", "action": "ajax", "args": { "url": "/api/b" } }
]}

运行时边车(Resource 状态、Reaction 状态、缓存、诊断)跟随词法所有权,但不得成为挂载在 ScopeRef 上的方法或可变协议对象。Scope 承载数据环境,不承载 bridge、controller、handle 或其他命令型对象。


6. 领域隔离与抽象

Flux 核心维持一个小的、稳定的抽象层。它的目标不是吞掉所有前端领域语义,而是提供一个足够稳的执行内核,让不同领域可以在核心之外成长。

这条原则的判断标准不是"核心能不能直接描述所有复杂系统",而是"核心能不能为复杂系统提供稳定嵌入面,而不把领域复杂度反向灌回核心词汇"。

隔离契约

领域系统(Flow Designer、Report Designer、Spreadsheet Editor、协作引擎、CRDT/OT 等)与 Flux 核心的交互被收敛为:

方向 机制 含义
核心 → 领域(读) Host Projection 只读快照投影,宿主驱动刷新
领域 → 核心(写) Capability 命名空间化的命令派发(如 designer:*
实例定位 ComponentHandleRegistry 显式目标组件实例方法调用
宿主私有 DomainBridge getSnapshot/subscribe/dispatch,不进入 Schema-visible Scope

核心保持稳定的理由

  • 图算法、布局、碰撞检测、协作协议、CRDT/OT、local-first 同步、手势循环------这些都是重要的,但它们是领域系统,不应成为核心原语。它们在 Flux 看来只是 Resource 背后的生产策略、Host Projection 背后的宿主快照、或 Capability 背后的命令系统。
  • 新域通过声明宿主类型 + 投影字段 + 能力命名空间接入,无需引入新的全局 provider 族、环境注册表或新的 schema 权威通道。
  • 可编辑宿主的跨域通用写入模式:读 Host Projection → 写 Capability(结构化 patch DTO)→ DomainBridge 宿主私有。

业务语义归属

业务管道(表单提交、对话框确认、页面进入)属于拥有该生命周期边界的节点,而非 UI 触发器。具体示例见第4节 Semantic Lifecycle Entry。这是词法所有权和领域隔离在具体模式上的体现。


汇总

# 原则 一句话
1 DSL 优先 DSL 是独立于运行时的一等结构层,先可编辑、可组合、可变换,再进入执行期。
2 编写-执行分离 编写态与执行态服务不同优化目标,二者应由预编译边界分层,而不是由运行时混同承担。
3 响应式数据驱动 Value 原语内建依赖跟踪,读写分离,副作用收敛到 Capability。
4 渐进式演化 复杂度应从简单 DSL 形式和既有原语自然生长,而不是通过膨胀 primitive 集合获得。
5 词法所有权 数据、能力、资源、反应及其 sidecar 跟随词法/子树边界归属,而不是泄漏到全局运行时大对象。
6 领域隔离与抽象 核心提供小而稳的执行内核,领域复杂度留在核心之外,通过窄契约嵌入。

nop-chaos-flux 已开源:

相关推荐
东方小月1 小时前
5分钟搞懂Harness Engineering(驾驭工程):从提示词到AI Agent的进化之路
前端·后端·架构
我叫黑大帅1 小时前
为什么需要 @types/react?解决“无法找到模块 react 的声明文件”报错
前端·javascript·面试
之歆1 小时前
DAY_21JavaScript 深度解析:数组(Array)与函数(Function)(一)
前端·javascript
XinZong2 小时前
【AI社交】基于OpenClaw自研轻量化AI社交平台实战
前端
Le_ee2 小时前
ctfweb:php/php短标签/.haccess+图片马/XXE
开发语言·前端·php
爱上好庆祝3 小时前
学习js的第七天(wed APIs的开始)
前端·javascript·css·学习·html·css3
KaMeidebaby3 小时前
卡梅德生物技术快报|冻干工艺开发:注射用心肌肽全流程参数优化与工程化方案
前端·其他·百度·新浪微博
Moment4 小时前
面试官:如果产品经理给你多个需求,怎么让AI去完成❓❓❓
前端·后端·面试
每天吃饭的羊4 小时前
JSONP
前端