docs: 新增 DatAlive 设计器核心架构设计文档
- 新增设计器静态依赖分析方案,阐述基于 AST 的代码片段解析与依赖图谱构建 - 新增实体生命周期引擎实现规范,定义引擎驱动的事件分发与交互触发策略 - 新增悬浮组件架构设计规范,明确底层数据统一与上层视图分离的核心原则 - 新增应用运行模式架构规范,严格区分设计态、预览态与运行态的边界 - 新增设计器选中与分组交互规范,定义选中态、激活态及下钻交互的行为矩阵
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
# DatAlive 实体生命周期引擎实现规范 (Entity Lifecycle Implementation)
|
||||
|
||||
## 1. 设计理念:引擎驱动生命周期 (Engine-Driven Lifecycle)
|
||||
|
||||
在基于 React 19+ 生态的低代码架构中,由于 Strict Mode、Concurrent Rendering 和 Suspense 的存在,React 组件的真实挂载/卸载行为往往不可预测(例如组件可能被多次挂载/卸载以检查副作用)。
|
||||
|
||||
因此,DatAlive 平台中的“业务生命周期”**严禁直接等价于 React 的生命周期(如 `useEffect`)**。我们必须采用**引擎驱动**模式:由底层的 `RuntimeEngine` 根据路由状态、全局数据流或用户行为,**显式地派发(Emit)**各个实体的生命周期事件。React 仅仅作为“被动接收状态并渲染”的视图层。
|
||||
|
||||
在底层 Schema 的定义中,无论是路由跳转、数据请求回调还是用户点击,这一切所谓的“生命周期钩子”,均被统一收口并泛化为 **`Interaction`(泛化事件侦听器)**。
|
||||
|
||||
---
|
||||
|
||||
## 2. 实体生命周期映射与 Interaction 触发策略
|
||||
|
||||
目前设计器中允许配置 `interactions: Interaction[]` 字段的实体,及其引擎侧的触发策略定义如下:
|
||||
|
||||
### 2.1 Application (应用根节点)
|
||||
负责整个低代码应用启动和全局级别的状态拦截。
|
||||
|
||||
| Interaction.event (触发器) | 触发时机 | 引擎实现策略 |
|
||||
| :--- | :--- | :--- |
|
||||
| `onLaunch` | 应用配置加载完成,准备渲染根节点前 | 在 React `createRoot().render()` 执行前,由引擎主动解析执行。可用于初始化全局变量、鉴权校验等。 |
|
||||
| `onError` | 全局未捕获的运行时异常(含数据流异常) | 绑定在 React ErrorBoundary 顶层,以及 Zustand / TanStack Query 的全局 Error Handler 中。 |
|
||||
|
||||
### 2.2 Route & Page (路由与页面容器)
|
||||
视图层的状态闭环单元,处理路由切换带来的副作用(Route Guards)。
|
||||
|
||||
| Interaction.event (触发器) | 触发时机 | 引擎实现策略 |
|
||||
| :--- | :--- | :--- |
|
||||
| `onEnter` (Route级) | 路由准备进入该页面前 (Route Guards) | 挂载在路由监听层。可在此处派发 Action 发起鉴权,若鉴权失败可触发 Redirect Action 踢回登录页。 |
|
||||
| `onLoad` (Page级) | 页面组件挂载,且前置预热数据已就绪 | 在页面容器初始化时触发。常用于重置当前页面的局部 Variables,或发起非预热的懒加载 Query。 |
|
||||
| `onLeave` (Route级) | 路由准备离开该页面之前 | 路由切换前触发。用于清理定时器、WebSocket 或清空重型数据变量。 |
|
||||
| `onResize` (Page级) | (大屏特有) 画布或窗口尺寸发生变化 | 引擎统一在 `window.addEventListener('resize')` 中做防抖处理,然后派发给当前激活的 Page。 |
|
||||
|
||||
### 2.3 Component (视图原子组件)
|
||||
最核心的用户交互载体,其事件主要由 DOM 交互驱动,少部分由挂载周期驱动。
|
||||
|
||||
| Interaction.event (触发器) | 触发时机 | 引擎实现策略 |
|
||||
| :--- | :--- | :--- |
|
||||
| **交互类**: `onClick`, `onMouseEnter`, `onChange` 等 | 用户与组件进行真实物理交互时 | 通过标准 React Props 传递给具体的组件实现,例如 `<Button onClick={(e) => engine.dispatchInteraction('btn_1', 'onClick', e)} />`。 |
|
||||
| **生命周期类**: `onMount`, `onUnmount` | 组件实例在画布中真实渲染/销毁时 | 不直接使用原生的 `useEffect`。需封装专门的 `useLowcodeMount` Hook,内部处理 React 18+ Strict Mode 的双次调用问题(幂等拦截),再向引擎上报。 |
|
||||
|
||||
### 2.4 Variable (响应式全局变量)
|
||||
低代码状态驱动逻辑的核心连锁反应触发器。
|
||||
|
||||
| Interaction.event (触发器) | 触发时机 | 引擎实现策略 |
|
||||
| :--- | :--- | :--- |
|
||||
| `onChange` | 变量的值发生实质性突变时 | 在 Zustand Store 的 Reducer 层拦截,或通过 `store.subscribe` 监听。**必须进行深比较(Deep Equal)防止重复触发**。这是实现复杂表单联动(如省市级联清空)的核心机制。 |
|
||||
|
||||
### 2.5 Query / Mutation (CQRS 数据源请求)
|
||||
异步控制流的核心,与后端交互的桥梁。
|
||||
|
||||
| Interaction.event (触发器) | 触发时机 | 引擎实现策略 |
|
||||
| :--- | :--- | :--- |
|
||||
| `onSuccess` | 数据请求成功并返回 2xx 状态码时 | 完美契合 TanStack Query。在其 `onSuccess` 回调中拦截并交由引擎执行配置的 Actions(如弹出成功 Toast,或触发其他 Query 的刷新)。 |
|
||||
| `onError` | 请求失败、网络异常或业务报错时 | 在 TanStack Query 的 `onError` 回调中拦截。可在此处派发全局异常处理 Action。 |
|
||||
| `onSettled` | 无论成功或失败,请求流程结束时 | 在 TanStack Query 的 `onSettled` 回调中拦截。用于统一下发如“关闭某个特定按钮的 Loading 态”之类的重置动作。 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 架构优势总结
|
||||
|
||||
通过将以上所有的生命周期和事件收敛为 Schema 中的 `Interaction` 实体模型,我们获得了以下压倒性的架构优势:
|
||||
|
||||
1. **万法归宗 (Unified Control Flow)**:引擎只需要一个通用的“事件分发器(Event Dispatcher)”,就能处理从 DOM 鼠标点击到网络回调的所有异步行为。
|
||||
2. **确定性 (Determinism)**:业务逻辑的执行不受 React 并发渲染机制的干扰。
|
||||
3. **可测试性 (Testability)**:可以在 Node.js 环境下加载 Schema,通过调用 `engine.dispatchInteraction('query_1', 'onSuccess')` 直接测试复杂的联动数据流,完全无需真实的 DOM 参与。
|
||||
4. **职责单一 (Separation of Concerns)**:React 只负责 `UI = f(state)`,`RuntimeEngine` 负责 `Event -> Action -> State Update` 的逻辑编排。
|
||||
@@ -0,0 +1,80 @@
|
||||
# DatAlive 应用运行模式 (AppMode) 架构设计规范
|
||||
|
||||
在低代码/无代码平台的设计中,渲染引擎需要根据不同的上下文环境展现出不同的行为特征。为了保证底层架构的清晰与解耦,DatAlive 明确确立了 **AppMode(应用运行模式)** 的三态规范,并对“预览模式”与“运行模式”的功能边界进行了严格的界定。
|
||||
|
||||
## 1. 架构定位:AppMode 是运行时上下文,绝非 Schema
|
||||
|
||||
在架构设计的初期,极易将运行模式与应用的数据结构混为一谈。必须极其严厉地明确以下铁律:
|
||||
|
||||
> **`AppMode` 绝对不属于需要存入数据库持久化的 `design-mode` Schema 蓝图!**
|
||||
> Schema 描述的是“应用长什么样,有什么逻辑”,而 `AppMode` 决定的是“渲染引擎现在该用什么姿势来解析这份 Schema”。
|
||||
|
||||
在渲染引擎的前端代码实现中,`AppMode` 应当作为顶级组件的 Props(或全局环境变量)由外部宿主容器(Host Shell)显式注入:
|
||||
|
||||
```tsx
|
||||
// AppMode 的三态枚举(定义于引擎层,而非 Schema 层)
|
||||
export type AppMode =
|
||||
| 'design' // 设计态:引擎进入“沙盒模式”,劫持交互与路由,渲染视觉辅助线。
|
||||
| 'preview' // 预览态:放行业务事件,提供沉浸式体验,但挂载强大的白盒调试工具。
|
||||
| 'runtime'; // 运行态:绝对纯净的运行环境,无任何设计器代码注入。
|
||||
```
|
||||
|
||||
### 1.1 为什么拒绝使用 `publish` 作为模式?
|
||||
在架构设计中,必须将 **“底层渲染状态(Render State / AppMode)”** 与 **“应用版本生命周期(Lifecycle Status)”** 彻底解耦。
|
||||
* `runtime`(运行时)是一个渲染引擎的“物理运行环境”。
|
||||
* `publish`(发布)是一个应用的“版本流转动作”(如 `draft`, `testing`, `published`)。
|
||||
即便是本地测试的一个未发布的草稿版本,当它脱离设计器独立在手机上扫码预览时,其底层的渲染模式依然是 `runtime`。
|
||||
|
||||
---
|
||||
|
||||
## 2. 预览模式 (Preview) vs 运行模式 (Runtime):同源异构的双生子
|
||||
|
||||
如果抛开后端服务和数据存储介质不谈,纯粹从前端功能视角来看,**预览模式**和**运行模式**在底层逻辑上是 100% 同源的(共享同一套 Schema 渲染管线),但在外在表现上却截然不同。
|
||||
|
||||
**一个形象的比喻:**
|
||||
* **预览模式(Preview)**:是导演(开发者)在看**监视器**。画面里可能有收音麦克风穿帮、有场记板、有剧本标注。发现演员台词念错了,导演会喊“咔(报错)”,然后立刻重来。
|
||||
* **运行模式(Runtime)**:是观众在电影院看**最终上映的电影**。画面绝对干净,剧情连贯,即便有个别穿帮镜头(Bug),电影也会继续放下去,绝不会突然黑屏跳出一段剧本代码。
|
||||
|
||||
### 2.1 核心功能差异对比
|
||||
|
||||
| 维度 | 预览模式 (Preview Mode) | 运行模式 (Runtime Mode) | 设计模式 (Design Mode) |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **目标用户** | 应用搭建者、实施人员 | 最终业务人员(如客服、仓管) | 平台实施人员 |
|
||||
| **路由引擎 (Routing)** | 原生 `BrowserRouter` 或 Hash,独立窗口展示。 | 干净的业务地址(如 `/apps/my-dashboard`)。 | **强制隔离**:必须使用 `MemoryRouter`,防止点击 a 标签导致设计器真刷新或逃逸。 |
|
||||
| **异常处理 (Error)** | **严厉的教练**:大张旗鼓地报错(Error Boundary 标红),将 JS 堆栈暴露在控制台。 | **温柔的服务员**:静默容错,降级 UI 或弹出温和的 Toast,绝不暴露底层堆栈。 | 忽略破坏性错误,保护画布不崩溃。 |
|
||||
| **破坏性操作 (Mutation)**| 允许执行,但通常通过拦截器指向 Mock 或沙箱环境。 | 毫无保留地真实执行业务逻辑。 | **强力封杀**:引擎必须静默拦截所有的 `Mutation` 动作,防止在画布点击按钮删除了真实数据。 |
|
||||
| **环境纯净度** | 包裹在调试器上下文中,注入各种 DevTools。 | 轻量级纯净容器。剔除所有设计器相关的 JS 胖包和无用 DOM。 | 挂载 `react-moveable` 等庞大的拖拽与辅助渲染引擎。 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 预览模式专属辅助工具 (Preview Utilities) 深度展开
|
||||
|
||||
为了让“导演”能更好地排查“演员”的问题,预览模式必须配备一系列强大的领域特定(Domain-Specific)辅助工具。传统的浏览器 F12 DevTools 对低代码编译后的虚拟 DOM 是“瞎”的,因此必须集成以下工具:
|
||||
|
||||
### 3.1 低代码控制台 (Low-Code Console / Debugger)
|
||||
这是预览模式的核心灵魂,负责将低代码引擎内部的“黑盒状态”白盒化。
|
||||
1. **全局状态漫游器 (State Explorer)**:
|
||||
* 实时展示当前应用中所有的全局变量(`Variables`)、查询快照(`Queries`)和组件暴露的运行时属性。
|
||||
* 允许开发者手动 Override(覆盖)某个变量的值,实时观察页面的响应式变化。
|
||||
2. **数据流监控 (Network / Query Monitor)**:
|
||||
* 拦截并展示所有的 `Query` 和 `Mutation` 执行情况(完美契合 TanStack Query DevTools)。
|
||||
3. **动作轨迹时间轴 (Action Tracer / Timeline)**(最难也是最核心):
|
||||
* 解决低代码中最难排查的“连锁反应”Bug。
|
||||
* 以瀑布流日志打印执行顺序:`[事件触发] -> [变量修改] -> [Query执行] -> [成功回调代码执行]`,让数据流转路径一目了然。
|
||||
4. **代码沙箱异常日志 (Code Sandbox Logger)**:
|
||||
* 专门捕获用户手写的 `DynamicExpression` (如 `type: 'code'`) 中抛出的 JS 异常,将其劫持并优雅地打印在低代码控制台中。
|
||||
|
||||
### 3.2 组件审查器 (Component Inspector)
|
||||
在预览模式下,开发者有时需要快速定位 UI 问题,但又不想切回设计模式:
|
||||
* **悬浮高亮 (Hover Outline)**:按住特定快捷键(如 `Alt` / `Option`)在页面上移动鼠标,被掠过的组件会显示高亮边框和其 `Entity.id`。
|
||||
* **快捷跳转 (Quick Jump)**:点击被高亮的组件,利用 `postMessage` 与父级设计器通信,设计器立即在画布中选中该组件,右侧面板自动展开其配置项。
|
||||
|
||||
---
|
||||
|
||||
## 4. MVP 阶段的务实落地策略
|
||||
|
||||
在 DatAlive 的 MVP(最小可行性产品)阶段,我们采取 **“架构上严格区分,实现上极简降级”** 的策略:
|
||||
|
||||
1. **引擎入口的强约束**:在渲染引擎的顶层组件定义中,严格要求传入 `mode: AppMode`,并根据此字段进行核心模块(如 Router 和 拖拽层)的条件分支渲染。
|
||||
2. **借力生态实现控制台**:暂不自研复杂的 Action Tracer,而是直接在预览模式接入 `<ReactQueryDevtools />`(监控请求状态机)和开启 Redux/Zustand DevTools Middleware(监控变量变化),以极低成本获得企业级调试能力。
|
||||
3. **前端纯净路由闭环**:即使没有后端,运行模式(Runtime)也必须在一个独立的 HTML 页面下读取 `localStorage` 进行渲染,以此强制验证渲染引擎是否真正脱离了设计器的胖包上下文。
|
||||
@@ -0,0 +1,79 @@
|
||||
# DatAlive 悬浮组件 (Overlay Components) 架构设计规范
|
||||
|
||||
在低代码平台(如 DatAlive)中,悬浮组件(如 `Modal` 弹窗、`Drawer` 抽屉、全局 `Toast` 等)由于其“脱离标准文档流、需要交互唤起、默认隐藏”的特性,一直是设计器架构、状态管理和交互设计的难点。
|
||||
|
||||
本规范基于 DatAlive 的核心设计哲学,详细界定了悬浮组件在底层数据蓝图(Schema)、物料元数据(Component Meta)、运行时状态(Runtime Store)以及设计器用户界面(UI)之间的流转与协作机制。
|
||||
|
||||
---
|
||||
|
||||
## 1. 核心原则:底层数据统一,上层视图分离
|
||||
|
||||
在架构设计初期,开发者极易陷入“为特殊组件定制特殊结构”的陷阱。DatAlive 坚决摒弃在 Schema 中为悬浮组件设立“特权结构”(例如在 Page 节点下新增 `modals: Component[]` 数组)的做法。
|
||||
|
||||
### 1.1 Schema 实例层 (Instance Schema) 的绝对纯净
|
||||
在最终持久化存入数据库的 `design-mode` Schema 中,**万物皆为平等的 `Component` 节点**。
|
||||
|
||||
* **统一的树模型**:悬浮组件仅仅是 `Page.components` 树中的一个普通节点。它拥有与普通组件完全一致的接口:`type`, `props`, `layout`, `style`, `children`。
|
||||
* **嵌套无特权**:当用户向 `Modal` 内部拖入一个 `Table` 时,底层发生的数据结构变更仅仅是:向该 `Modal` 节点的 `children` 数组中 `push` 了一个 `Table` 节点。这份包含了弹窗及其内部所有子组件的完整 JSON 树,将原封不动地被导出或保存。
|
||||
* **架构优势**:
|
||||
* **极简的渲染引擎**:引擎只需要一套极其简洁的递归遍历逻辑,即可完成包含弹窗在内的整棵组件树的解析,无需编写针对 `modals` 的额外逻辑。
|
||||
* **交互动作泛化**:因为弹窗只是普通节点,所有的泛化 Action(如 `{"target": {"id": "modal_1"}, "method": "open"}`)可以无缝作用于它,无需修改目标选择器逻辑。
|
||||
|
||||
### 1.2 物料元数据层 (Component Meta) 的二元解耦
|
||||
悬浮组件之所以在 UI 上表现特殊,是因为它在**平台物料库的注册表(Component Registry)**中带有明确的静态元数据标识。我们必须严格区分“组件的静态特征”与“实例的运行时状态”。
|
||||
|
||||
```typescript
|
||||
// 伪代码:在设计器源码(或 SDK)中注册 Modal 物料
|
||||
Registry.register({
|
||||
type: 'Modal',
|
||||
name: '对话框',
|
||||
category: '反馈',
|
||||
icon: 'icon-modal',
|
||||
isOverlay: true, // 🚨 核心标识:告知设计器这是一个悬浮容器
|
||||
isContainer: true, // 允许向其内部拖入子组件
|
||||
setters: [ ... ] // 属性配置器
|
||||
});
|
||||
```
|
||||
|
||||
* **彻底的解耦机制**:像 `isOverlay` 这种描述“物料类”固有属性的标识,**绝对不能**被抽离或序列化到用户的页面 Schema JSON 中。Schema 只保存实例特有的变动数据(如 `props.title`)。设计器在运行时通过 `type` 查字典(Type Mapping),动态获知该实例属于 `isOverlay: true` 的类别。
|
||||
|
||||
---
|
||||
|
||||
## 2. 设计器 UI 层面的协作机制 (MVC 映射)
|
||||
|
||||
基于上述底层架构,设计器在处理悬浮组件时,展现出极其优雅的 MVC(Model-View-Controller)协作,确保“Schema”、“图层面板”和“主画布”三方的绝对同步。
|
||||
|
||||
### 2.1 唯一真相来源 (Single Source of Truth)
|
||||
Zustand/Redux 内存中的 `Application Schema`(特别是当前 Page 的被扁平化或深层嵌套的 `components` 树)是唯一的 Model。视图层(图层面板、画布)本身不保存任何层级或显隐状态。
|
||||
|
||||
### 2.2 图层树面板的“动态分叉 (Forking)”
|
||||
为了避免主画布被多个隐藏的弹窗遮挡,设计器的左侧【图层面板】必须与普通组件隔离展示悬浮组件。
|
||||
|
||||
* **纯粹的受控渲染**:图层面板订阅当前页面的 `components` 树。
|
||||
* **动态过滤与分组**:遍历每个节点,通过 `type` 查验 Meta 字典。若 `Registry[type].isOverlay === true`,则将该节点分发至独立的“悬浮层(Overlays)”虚拟文件夹中进行渲染;否则留在“主图层”中。
|
||||
* **所见即所得**:无论组件在图层树的哪个文件夹,它们在底层的 Schema 数组中依然是平级的。修改图层树的任何属性(如重命名、拖拽排序),都会 Dispatch Action 修改底层 Schema,触发全链路重绘。
|
||||
|
||||
### 2.3 画布渲染引擎的 Portal 挂载
|
||||
在运行模式(`runtime`)或预览模式(`preview`)下,渲染引擎解析到 `type: 'Modal'` 时,其对应的 React 组件内部实现应利用 `ReactDOM.createPortal` 技术。
|
||||
这使得 `Modal` 及其 `children` 被“传送”挂载到 `document.body` 节点下,从而脱离当前容器的 `overflow` 限制和局部的层叠上下文(Stacking Context),实现真正的全屏悬浮覆盖。
|
||||
|
||||
---
|
||||
|
||||
## 3. 设计态的交互困境与双路径解法
|
||||
|
||||
在设计态下,悬浮组件默认是隐藏的(通常由 `props.visible` 控制)。为了让实施人员能够向其内部拖入子组件(如表单、表格),平台提供两条并行的交互路径:
|
||||
|
||||
### 路径 A:画布强制唤醒 (Canvas Override) —— 面向直观操作
|
||||
这是为主流用户提供的所见即所得体验:
|
||||
1. **唤醒**:用户在左侧图层树的“悬浮层”目录中,点击该 `Modal` 节点旁的“眼睛(显示)”图标(修改 Schema 中的 `layer.hidden = false`)。
|
||||
2. **劫持**:设计器拦截其原有的业务可见性逻辑(如忽略绑定的 `visible` 变量),强制在主画布最顶层渲染该弹窗,并赋予极高的 z-index 以防止遮挡。
|
||||
3. **拖拽**:用户从物料区拖拽 `Table` 组件,直接在弹窗的实体区域(DropZone)内释放(Drop)。
|
||||
4. **写入**:拖拽引擎捕获释放事件,定位到目标容器,在底层修改 Schema 树,将 `Table` 插入 `Modal` 的 `children` 数组。
|
||||
|
||||
### 路径 B:图层树直接盲投 (Tree Drop) —— 面向精准控制
|
||||
这是为应对复杂层级遮挡或高级用户提供的极客体验:
|
||||
1. **绕过画布**:用户从物料区拖拽组件,**不经过中间的主画布**。
|
||||
2. **树形释放**:直接将组件拖拽至左侧【图层面板】中目标 `Modal` 所在的文件树节点上悬停并释放。
|
||||
3. **写入**:图层面板组件直接捕获 Drop 事件,并派发同样的 Action,将组件精准插入底层 Schema 对应节点的 `children` 数组。
|
||||
|
||||
这两条路径完美互补,且底层收敛于唯一的树状结构修改代码。这种“上层交互多态,底层逻辑唯一”的设计,确保了平台在处理复杂嵌套容器时的绝对健壮性。
|
||||
@@ -0,0 +1,185 @@
|
||||
# DatAlive 设计器选中与分组交互规范 (Editor Selection & Drill-down Strategy)
|
||||
|
||||
> 本文档定义了 DatAlive 设计器中核心的“选中(Selection)”与“下钻(Drill-down)”交互逻辑,旨在解决多层级组件嵌套下的精准选中难题。该规范参考了 Figma 等主流设计工具的交互模式,并严格适配 DatAlive 的底层 Schema 架构。
|
||||
|
||||
## 1. 核心概念 (Core Concepts)
|
||||
|
||||
为了实现自然流畅的深层嵌套组件操作,我们必须在编辑器**运行时的内存状态**中区分两个维度的“选中”概念:
|
||||
|
||||
### 1.1 选中态 (Selection State)
|
||||
* **含义**:当前用户正在操作的目标组件。
|
||||
* **表现**:显示实线边框、8个缩放控制点(Resize Handles)、右侧属性面板显示该组件属性。
|
||||
* **操作**:此时按 Delete 删除该组件,按方向键移动该组件,或拖拽控制点改变其 `layout` 属性。
|
||||
* **数据结构**:`selectedIds: string[]`(支持多选)。
|
||||
|
||||
### 1.2 激活态 / 下钻态 (Activation State / Drill-down Context)
|
||||
* **含义**:当前用户“进入”了某个容器组件(如 Group, Card, Modal)的内部上下文。
|
||||
* **表现**:该容器本身不再被选中(无控制点),但显示虚线边框(或高亮背景),提示用户“你正在编辑这个容器内部的内容”。该容器外部的元素通常会变暗或不可响应点击(Focus Mode 专注模式)。
|
||||
* **操作**:此时点击该容器内的任何子元素,将直接选中子元素,而不再选中该容器本身。
|
||||
* **数据结构**:`activeContainerId: string | null`。
|
||||
* `null`: 默认状态,处在当前 `Page` 的画布根层级。
|
||||
* `string`: 处在某个具体容器组件的内部。
|
||||
|
||||
---
|
||||
|
||||
## 2. 状态定义 (Store Definition)
|
||||
|
||||
前端状态管理(如 Zustand/Redux)在设计态(`AppMode === 'design'`)应维护以下瞬时字段(这些字段**绝对不存入**持久化的 Schema 数据库中):
|
||||
|
||||
```typescript
|
||||
interface EditorSelectionState {
|
||||
// 当前选中的组件 ID 列表
|
||||
selectedIds: string[];
|
||||
|
||||
// 当前激活的容器组件 ID(下钻上下文)
|
||||
// null 表示未激活任何深层容器,处于 Root Page Context
|
||||
activeContainerId: string | null;
|
||||
|
||||
// 【架构约束】:为实现 O(1) 的层级查找,
|
||||
// 引擎在加载 Schema 树 (Page.components) 时,必须在内存中构建一个扁平化的辅助字典,
|
||||
// 动态推导出每个组件的 parentId(持久化 Schema 中坚决不存 parentId)。
|
||||
// componentNodeMap: Record<string, { id: string, parentId: string | null, type: string }>;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 交互行为矩阵 (Interaction Matrix)
|
||||
|
||||
### 3.1 单击行为 (Click)
|
||||
**核心规则**:始终在 `activeContainerId` 定义的上下文中,寻找最高层级的子节点(冒泡拦截)。
|
||||
|
||||
| 场景 | 当前状态 | 动作 | 结果状态 | 逻辑说明 |
|
||||
| :--- | :--- | :--- | :--- | :--- |
|
||||
| **基础选中** | `active: null` | 点击 Button A (根节点) | `sel: ['Button A']`<br>`active: null` | 最简单的选中。 |
|
||||
| **选中容器** | `active: null` | 点击 Button B (属于 Group 1) | `sel: ['Group 1']`<br>`active: null` | 点击容器内元素,自动冒泡选中其最外层容器。 |
|
||||
| **组内选中** | `active: 'Group 1'` | 点击 Button B (属于 Group 1) | `sel: ['Button B']`<br>`active: 'Group 1'` | 因为已激活 Group 1,点击其子元素直接选中子元素。 |
|
||||
| **深层冒泡** | `active: 'Group 1'` | 点击 Button C (属于 Group 2,且 Group 2 在 Group 1 内) | `sel: ['Group 2']`<br>`active: 'Group 1'` | 冒泡到当前激活层级(Group 1)的直接子节点(Group 2)。 |
|
||||
| **退出下钻** | `active: 'Group 1'` | 点击 Button A (根节点,在 Group 1 外) | `sel: ['Button A']`<br>`active: null` | 点击目标不在当前激活容器内,自动重置 `activeContainerId` 为 null,并选中目标。 |
|
||||
| **空白重置** | 任意 | 点击画布空白处 | `sel: []`<br>`active: null` | 清空所有状态。 |
|
||||
|
||||
### 3.2 双击行为 (Double Click)
|
||||
**核心规则**:进入(Drill-down)当前选中的容器组件内部。
|
||||
|
||||
| 场景 | 当前状态 | 动作 | 结果状态 | 逻辑说明 |
|
||||
| :--- | :--- | :--- | :--- | :--- |
|
||||
| **进入容器** | `sel: ['Group 1']` | 双击 Group 1 | `sel: []` <br>`active: 'Group 1'` | **关键交互**:进入 Group 1 内部。此时 Group 1 变为容器上下文。 |
|
||||
| **连续下钻** | `sel: ['Group 2']`<br>`active: 'Group 1'` | 双击 Group 2 | `sel: []`<br>`active: 'Group 2'` | 在 Group 1 内部继续下钻进入 Group 2。 |
|
||||
| **文本编辑** | `sel: ['Button']` | 双击 Button | `active: 不变` | 【架构约束】:只有 `ComponentRegistry[type].isContainer === true` 的组件才允许双击下钻。原子组件双击通常触发“文本行内编辑”或忽略。 |
|
||||
|
||||
### 3.3 修饰键直选 (Cmd/Ctrl + Click)
|
||||
**核心规则**:穿透所有嵌套容器,直接选中鼠标下方最深层的原子组件。
|
||||
|
||||
| 场景 | 当前状态 | 动作 | 结果状态 | 逻辑说明 |
|
||||
| :--- | :--- | :--- | :--- | :--- |
|
||||
| **穿透选中** | `active: null` | Cmd + 点击 Button C (在 Group 2 中) | `sel: ['Button C']`<br>`active: 'Group 2'` | 直接选中 Button C。为了方便后续操作,建议自动将 `activeContainerId` 设置为 Button C 的动态 `parentId`(即 Group 2)。 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 算法实现逻辑 (Implementation Logic)
|
||||
|
||||
以下伪代码描述了画布引擎在接收到底层 DOM 点击事件时的核心处理流程,完美适配了 DatAlive 的无 `parentId` 纯净 Schema 树架构:
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* 处理画布上的组件点击事件
|
||||
* @param targetId - 鼠标直接点击到的最深层 DOM 对应的组件 ID
|
||||
* @param isDoubleClick - 是否为双击事件
|
||||
* @param isCmdPressed - 是否按住 Cmd/Ctrl 键
|
||||
*/
|
||||
function handleComponentClick(targetId: string, isDoubleClick: boolean, isCmdPressed: boolean) {
|
||||
const {
|
||||
componentNodeMap, // 引擎在内存中扁平化生成的辅助字典 (包含动态 parentId)
|
||||
activeContainerId,
|
||||
setActiveContainerId,
|
||||
setSelectedIds
|
||||
} = useEditorStore.getState();
|
||||
|
||||
const targetNode = componentNodeMap[targetId];
|
||||
if (!targetNode) return;
|
||||
|
||||
// 1. 处理 Cmd/Ctrl 直选(穿透所有层级)
|
||||
if (isCmdPressed) {
|
||||
setSelectedIds([targetId]);
|
||||
// 体验优化:直选后,自动激活其直接父级容器,方便后续点击旁边的兄弟元素
|
||||
setActiveContainerId(targetNode.parentId || null);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 处理双击下钻 (Drill-down)
|
||||
if (isDoubleClick) {
|
||||
// 【架构防呆】:必须去物料元数据注册表中查询该组件是否为合法的容器!
|
||||
// 坚决不能写死 targetNode.type === 'Group'
|
||||
const meta = ComponentRegistry[targetNode.type];
|
||||
if (meta?.isContainer) {
|
||||
setActiveContainerId(targetId);
|
||||
setSelectedIds([]); // 清空选中,等待用户在组内进行下一步点击
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 处理普通单击(向上冒泡拦截逻辑)
|
||||
let currentId: string | null = targetId;
|
||||
let candidateId = targetId;
|
||||
|
||||
// 沿着辅助字典的 parentId 向上遍历,寻找在当前激活上下文中的正确选中目标
|
||||
while (currentId) {
|
||||
const node = componentNodeMap[currentId];
|
||||
const parentId = node.parentId;
|
||||
|
||||
// 关键判断 A:如果当前节点的父级就是 activeContainerId,
|
||||
// 说明当前节点就是我们要选中的目标 (因为它刚好在当前“激活结界”的第一层)
|
||||
if (parentId === activeContainerId) {
|
||||
candidateId = currentId;
|
||||
break;
|
||||
}
|
||||
|
||||
// 关键判断 B:如果父级是 null (说明遍历到了 Root Page),
|
||||
// 且当前没有任何容器被激活,目标就是这个最外层的根组件
|
||||
if (parentId === null) {
|
||||
candidateId = currentId;
|
||||
break;
|
||||
}
|
||||
|
||||
// 继续向上冒泡
|
||||
currentId = parentId;
|
||||
}
|
||||
|
||||
// 4. 跨层级逃逸检测(用户点击了 activeContainer 外部的其他组件)
|
||||
// 引擎利用预计算的 nodePath 或向上追溯,判断算出来的 candidateId 是否根本不是 activeContainer 的后代
|
||||
if (activeContainerId && !isDescendant(componentNodeMap, activeContainerId, candidateId)) {
|
||||
// 强制退出当前的下钻状态,重置为画布根层级
|
||||
setActiveContainerId(null);
|
||||
// 在根层级的上下文中,重新计算一次单击冒泡逻辑
|
||||
return handleComponentClick(targetId, false, false);
|
||||
}
|
||||
|
||||
// 5. 最终应用选中状态,触发属性面板和控制点 UI 的重绘
|
||||
setSelectedIds([candidateId]);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 视觉反馈规范 (Visual Feedback)
|
||||
|
||||
为了让实施人员清晰感知当前的层级上下文,画布引擎必须提供极其明确的视觉反馈:
|
||||
|
||||
| 状态 | 选中组件表现 | 激活容器表现 (Active Container) | 其他非相关元素表现 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **普通选中** | 实线蓝色边框<br>8个白色方块控制点<br>可拖拽移动 | N/A | 正常显示 |
|
||||
| **下钻编辑中** | 实线蓝色边框<br>8个白色方块控制点<br>可拖拽移动 | **粗虚线灰色边框**<br>无控制点(不可拖拽)<br>左上角显示层级面包屑 (e.g. "Group A > Card B") | **半透明 (Opacity 0.6)**<br>屏蔽鼠标事件 (Focus Mode) |
|
||||
| **悬停 (Hover)** | 蓝色细边框 (Outline)<br>不显示控制点 | N/A | 正常显示 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 键盘快捷键辅助 (Shortcuts)
|
||||
|
||||
在复杂的层级树中,提供高效的键盘导航是专业设计器的标配:
|
||||
|
||||
* **`Esc` (逃逸键)**:
|
||||
* 场景 1:如果 `selectedIds` 有值,清空选中(`selectedIds = []`)。
|
||||
* 场景 2:如果 `selectedIds` 为空,但 `activeContainerId` 不为空(处于下钻态),则**向上跳出一级**(将 `activeContainerId` 设为其 `parentId`)。
|
||||
|
||||
* **`Enter` (深入键)**:
|
||||
* 如果当前仅选中了一个 `isContainer: true` 的组件,直接进入该容器(等同于鼠标双击)。
|
||||
@@ -0,0 +1,84 @@
|
||||
# DatAlive 设计器静态依赖分析方案 (Static Dependency Analysis)
|
||||
|
||||
## 1. 方案背景
|
||||
|
||||
在低代码平台(如 DatAlive)的架构设计中,实体间(如组件、交互、查询等)的动态变量关联是一个核心能力。为了提供超越简单模板语法(如 `{{variables.token}}`)的极致灵活性,本设计器在底层架构上全面拥抱了 **“基于纯函数的动态求值逃生舱”(Dynamic Expression / Code Escape Hatch)**。
|
||||
|
||||
通过允许用户编写原生的 JavaScript/TypeScript 函数片段(即 Schema 中的 `DynamicExpression` 接口,如 `RestRequestByCode`、`ActionParamsByCode` 以及 `props` 的动态求值),平台能够支撑极其复杂的循环、数据转换与条件判断。
|
||||
|
||||
然而,这种黑盒化的代码片段如果缺乏平台级的管控,将导致设计器丧失“无用变量剔除”、“变量重命名级联更新”、“依赖关系拓扑图谱”等企业级的高级可视化工程能力。
|
||||
|
||||
因此,平台必须在**设计器外壳(Editor Shell)**的底层引入**静态依赖分析(Static Dependency Analysis)**机制,将用户编写的代码从“黑盒字符串”转化为平台可理解的“语法树模型”。
|
||||
|
||||
---
|
||||
|
||||
## 2. 数据结构设计基石(二元联合类型)
|
||||
|
||||
为了保持 Schema 的极致纯净并支撑未来的 AST 解析,所有支持动态求值的字段(如 `Action` 的 `params`、`Query` 的 `request` 等)均采用了 `fixed`(纯静态)与 `code`(动态求值)的二元联合类型设计:
|
||||
|
||||
```typescript
|
||||
// 统一的参数联合类型示例 (ActionParams)
|
||||
export type ActionParams = ActionParamsByFixed | ActionParamsByCode;
|
||||
|
||||
export interface ActionParamsByFixed {
|
||||
type: 'fixed';
|
||||
/**
|
||||
* 纯静态配置字典。
|
||||
* 专用于设计器右侧属性面板上的简单固定值输入表单。
|
||||
*/
|
||||
payload: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface ActionParamsByCode extends DynamicExpression {
|
||||
// 继承自 DynamicExpression
|
||||
// type: 'code';
|
||||
// code: string; (纯函数体字符串)
|
||||
}
|
||||
```
|
||||
|
||||
**分析边界**:设计器的依赖分析引擎**仅需要**拦截并解析那些 `type === 'code'` 的节点,而对于 `fixed` 节点,由于其结构是强类型的字典,引擎可以直接通过对象遍历提取依赖(如遇到特定的标识符),大幅降低了全局解析的性能开销。
|
||||
|
||||
---
|
||||
|
||||
## 3. 静态依赖分析的核心实现路径 (Toolchain Pipeline)
|
||||
|
||||
在获取到 Schema 中的 `code` 字符串后,设计器工具链需按以下管线执行依赖分析:
|
||||
|
||||
### 3.1 引入轻量级 AST 解析器
|
||||
在设计器编译层引入标准的 AST 解析器(如 `@babel/parser` 或 `acorn`)。当用户在代码编辑器(如 Monaco Editor)中完成 `code` 编写并失去焦点或保存时,立即触发异步解析。
|
||||
|
||||
### 3.2 标识符与上下文提取 (Identifier Extraction)
|
||||
遍历生成的抽象语法树(AST),重点拦截对上下文对象(`context`)的成员访问表达式(`MemberExpression`)。
|
||||
- **静态属性访问**:例如 `context.variables.currentUser`,AST 会明确暴露出 `variables` 和 `currentUser` 节点。
|
||||
- **解构赋值分析**:例如 `const { variables } = context; return variables.token;`,通过作用域(Scope)和绑定(Binding)分析追踪其真实来源。
|
||||
|
||||
### 3.3 构建全局依赖图谱 (Dependency Graph)
|
||||
将提取出的依赖特征(如“依赖了变量 `token`”、“依赖了查询 `getUser`”)提炼为元数据(Metadata)。这部分元数据:
|
||||
- **缓存在设计器的内存 Store 中**:用于在左侧“变量管理面板”中实时显示该变量被多少个组件引用,或在用户尝试删除某变量时弹出“该变量正被 3 个组件使用,确认删除?”的拦截警告。
|
||||
- **持久化隔离**:这部分图谱数据**绝生存**于纯净的 Schema 数据库中,它仅作为设计态的辅助元数据。
|
||||
|
||||
### 3.4 变量重构与级联更新 (Refactoring & Cascading)
|
||||
当用户在全局变量面板中修改变量的 `key`(例如将 `token` 修改为 `accessToken`)时:
|
||||
1. **精准定位**:通过依赖图谱瞬间定位到所有使用了该变量的 `DynamicExpression` 节点。
|
||||
2. **智能重写**:利用 AST 转换工具(如 `@babel/traverse` 或 `jscodeshift`),将对应的 `MemberExpression` 节点属性名静默修改为新名称。
|
||||
3. **重新生成代码**:通过 `@babel/generator` 将修改后的 AST 重新序列化为代码字符串,更新回 Schema Store 触发保存。
|
||||
|
||||
---
|
||||
|
||||
## 4. 工程权衡与边界处理 (Trade-offs & Edge Cases)
|
||||
|
||||
### 4.1 性能考量
|
||||
在浏览器中高频运行 AST 解析器会带来极大的性能开销。
|
||||
- **策略**:AST 解析引擎应被放置在 **Web Worker** 中异步执行,绝对避免阻塞设计器的主线程(UI 线程);同时配合防抖(Debounce)机制,仅在用户结束连续输入后触发全量分析。
|
||||
|
||||
### 4.2 动态键名的分析盲区 (Analysis Blind Spots)
|
||||
对于高度动态的代码,静态分析存在天然的盲区:
|
||||
- **场景**:例如 `const key = 'user' + 'Id'; return context.variables[key];`。
|
||||
- **策略**:平台不承诺对计算属性(Computed Properties)和极度动态拼接的键名提供精准追踪。当 AST 遇到动态 `MemberExpression` 且无法推断其字面量时,设计器界面可以抛出警告级别的提示(Warning),告知实施人员:**“该动态依赖无法被设计器的重命名或剔除引擎接管,若修改上游变量名,需您自行维护此处的代码一致性。”**
|
||||
|
||||
---
|
||||
|
||||
## 5. 总结
|
||||
本方案确立了 DatAlive **“基于纯函数的动态求值逃生舱”** 结合 **“底层 AST 静态分析工具链”** 的工程路线。
|
||||
|
||||
它在赋予高级实施人员图灵完备的极客灵活性的同时,通过外围的解析引擎完美补齐了低代码平台在复杂逻辑下的重构与依赖管理能力。这是实现企业级低代码平台(Pro-Code 与 Low-Code 深度融合)不可或缺的架构护城河。
|
||||
Reference in New Issue
Block a user