Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

知识归纳:从全栈 CRUD 到 Oak 组件树

前面的主教程不是只做了一个任务列表。我们先用 Todo 完成 Web CRUD,再增加 Group 与 Todo 的一对多关系,最后把同一套业务节点带到小程序、Desktop 和 Native。

现在回头整理这些代码,可以把 Oak 理解为:用实体统一数据事实,用组件树统一前端数据节点与待提交操作,再用不同 render 适配不同平台。

1. 先看最终业务模型

数据库关系是:

Group 1 -------- N Todo
                  |
                  +-- groupId

每个 Todo 必须属于一个 Group。它在 Oak 中由两份实体源文件表达:

  • src/entities/Group.ts:分组名称;
  • src/entities/Todo.ts:任务标题、完成状态,以及指向 Group 的引用。

执行 npm run make:domain 后,不要凭经验猜反向关系名,要到生成领域中确认。这个项目生成的是:

todo$group

它表示“通过 Todo.group 这条关系,反向取得当前 Group 下的 Todo 列表”。

2. 数据库知识如何映射到 Oak

数据库概念教程项目Oak 对应
grouptodo实体 Storage
一行一个分组、一条任务实体 Schema
主键idEntityShape 提供的 id
外键todo.groupIdTodo.Schema.group 引用
一对多一个 Group 有多个 Todo生成关系 todo$group
SELECT 列id、name、titleprojection
WHERE只查某组任务组件树关系节点与 filter
ORDER BY新任务优先sorters
INSERT新增 Group/TodoaddItem 形成 create
UPDATE改名、改标题、完成任务update / updateItem
DELETE删除记录removeItem
migration给已有表加字段或关系数据库升级计划

实体与数据库迁移是两件事

修改实体后运行:

npm run make:domain
npm run make:locale

这会更新代码、类型、Storage 与多语言数据,但不会擅自修改一个已有生产数据库。已有数据库仍要生成、审核并执行升级计划。这与传统 ORM 中“改 model”和“执行 migration”是两件事完全一致。

projection 是明确的数据合同

例如 TodoList 只需要:

projection: {
    id: 1,
    title: 1,
    completed: 1,
    $$createAt$$: 1,
},

它类似明确写出 SQL 的 SELECT 列。projection 还会被生成类型检查,因此字段拼错、关系不存在或结果使用方式错误,都能更早暴露。

3. 普通 React 组件树与 Oak 组件树的区别

普通 React 组件树主要说明“谁渲染谁”。Oak 组件树还要说明:

  1. 当前组件对应哪一个实体数据节点;
  2. 查询结果缓存在哪个节点;
  3. create、update、remove 操作属于哪个节点;
  4. execute() 从树的哪一层收集并提交操作。

教程项目最终的树是:

frontend/home                                      Group ListNode
└── frontend/home.<groupId>                        Group SingleNode
    └── frontend/home.<groupId>.todo$group         Todo ListNode
        └── ...todo$group.<todoId>                  Todo SingleNode Upsert

Group ListNode

首页是 entity: 'group'isList: true,负责查询所有 Group、渲染 tabs、创建 Group 草稿,并持有 Group Modal 的保存和取消。

Group SingleNode

选中一个 tab 后,页面用:

<GroupPanel oakPath={`${oakFullpath}.${selectedGroupId}`} />

把 GroupPanel 挂到具体 Group 节点。GroupPanel 不重复查询 todo,也不自己保存 todo;它只负责继续挂载关系节点。

todo$group ListNode

GroupPanel 使用:

<TodoList oakPath={`${oakFullpath}.todo$group`} />

这里不是把 groupId 当普通 props 传给 TodoList,而是把 TodoList 挂到 Group 的反向关系节点。runningTree 因此知道这些 Todo 属于当前 Group。

Todo SingleNode Upsert

新增或编辑 Todo 时,TodoList 在 Modal 中挂载:

<TodoUpsert oakPath={`${oakFullpath}.${editingTodoId}`} />

Upsert 只负责字段编辑:标题输入调用 this.update({ title }),完成状态调用 this.update({ completed })。Modal 的打开、保存和取消由拥有列表操作的父组件负责。

4. oakPath 不是普通 props

oakPath 是组件在 runningTree 中的地址。它决定子组件连接哪个数据节点,也决定操作如何沿组件树组织。

可以把它类比为 React key、路由 path、ORM relation path 和表单字段 path 的结合,但它同时承担这些职责,所以不能随意拼一个“看起来唯一”的字符串。关系段必须使用生成领域中的真实名字,例如 todo$group

5. Upsert 为什么同时支持新增和更新

TodoList 新增时先调用:

const id = this.addItem({
    title: '',
    completed: false,
});

addItem() 在 Todo ListNode 中建立 create 操作并返回新 id。随后 TodoUpsert 挂到这个 id 的 SingleNode,输入字段时继续修改这条待创建记录。

编辑已有 Todo 时,挂载方式完全相同,只是 id 指向数据库已有记录,this.update(...) 形成 update 操作。因此同一个表单既能 insert,也能 update,这就是本教程中 Upsert 的含义。

6. 最容易误解的规则:操作属于哪个节点

新增 Group

Group ListNode.addItem() 创建 Group,因此保存 Group 时由首页 Group ListNode 执行:

await this.execute();

如果只在 GroupUpsert SingleNode 中执行,父列表持有的 create 可能没有被一起提交。

新增关系 Todo

Todo ListNode.addItem()group.todo$group 节点创建 Todo,但这条 create 还依赖父 Group 提供关系上下文。只执行 Todo SingleNode 不够;只执行关系 ListNode,也可能缺少把新 Todo 绑定到父 Group 所需的级联上下文。

教程项目取得 TodoList 的父路径并执行 Group SingleNode:

async saveTodo() {
    const todoListPath = this.state.oakFullpath;
    if (!todoListPath) {
        throw new Error('Todo list node is not mounted');
    }
    const separatorIndex = todoListPath.lastIndexOf('.');
    const groupPath = todoListPath.slice(0, separatorIndex);
    await this.execute(undefined, undefined, groupPath);
}

这样 runningTree 会从 Group 节点收集关系子树中的操作,级联提交时为 Todo 绑定 groupId

这条规则可以归纳为:

先判断 create/update/remove 建立在哪个节点
-> 再判断关系外键依赖哪一层祖先上下文
-> 从能覆盖完整操作子树的节点 execute

不要把“离保存按钮最近的组件”当成默认执行节点。

7. data、formData、properties 与 render

合同用途教程例子
data组件本地状态和稳定初值selectedGroupIdeditingTodoId
formData把实体结果整理成 render 需要的形状groupstodos
properties父组件传入的业务参数只有确实需要普通输入时声明
methodsrender 可调用的业务交互saveTodosetTitle
框架注入Oak 节点状态与标准方法oakFullpathoakLoading

当前 Oak 编译器会根据同目录 index.ts 生成 render props。标准 TSX render 写:

export default function Render(props) {

不要手写宽泛 props 类型。render 使用了未声明字段时,应回到真实来源修复:父组件输入放进 properties,查询派生结果放进 formData,本地状态放进 data,交互函数放进 methods

小程序 XML 也使用同一合同检查字段、事件、组件属性和 class。anynever@ts-nocheck 或虚假的 props 声明都只是隐藏问题,不会建立真实运行时合同。

8. 前端、后端和 Oak 的职责边界

问题应放的位置
Modal 是否打开、当前 tab页面或组件 data
Group/Todo 查询与待提交操作Oak component node
标题字段编辑Upsert this.update(...)
标题不能为空checker
完成任务后写审计记录trigger
一次处理多个实体的复合业务aspect
第三方 HTTP 回调endpoint
定期处理过期任务timer / watcher
数据库存取backend context + oak-db

普通 CRUD 不需要为每个实体重复写 Controller、DTO 和 API client,因为实体、operation、context 与 runningTree 已经形成统一合同。但跨入口都必须成立的业务规则,仍必须放到 checker、trigger 或其它后端扩展点,不能只写在四个平台的 render 中。

9. 四个平台共享什么

Web小程序DesktopNative
Group/Todo 实体共享共享共享共享
关系名 todo$group共享共享共享共享
Group/Todo 组件逻辑共享共享共享共享
节点执行层级共享共享共享共享
渲染技术React/AntDXML/WXMLReact/Fluent UIReact Native
平台事件适配DOM小程序 eventDOM/FluentNative event

跨端复用不是强迫四端使用同一套标签,而是共享业务事实、组件节点和操作语义。render 只负责把这些能力翻译成平台 UI。

因此正确顺序是:先在 Web 把实体关系和组件树验证稳定,再增加小程序、Desktop、Native render。否则每个平台都会重复放大同一个模型错误。

10. 完整数据链

flowchart TD
    A["Group.ts / Todo.ts"] --> B["make:domain"]
    B --> C["EntityDict / Storage / todo$group"]
    C --> D["Group ListNode"]
    D --> E["Group SingleNode"]
    E --> F["todo$group ListNode"]
    F --> G["Todo Upsert SingleNode"]
    G --> H["runningTree pending operations"]
    H --> I["execute correct ancestor"]
    I --> J["backend context"]
    J --> K["checker / trigger / oak-db"]
    K --> L["SQLite / MySQL / PostgreSQL"]
    L --> M["opRecords and cache refresh"]
    M --> N["Web / MP / Desktop / Native render"]

11. 新增需求时的固定步骤

假设要给 Todo 增加截止日期:

  1. 从数据库角度判断字段类型、是否可空、旧数据和索引。
  2. 修改 Todo 实体与 locale。
  3. 运行 make:domainmake:locale,检查生成类型。
  4. 把日期加入 TodoList 和 TodoUpsert projection。
  5. 在 Upsert index.ts 增加统一 setter。
  6. Web 先实现日期控件并完成真实 CRUD。
  7. 若日期规则必须全端成立,写 checker,不在 render 中复制。
  8. 再为小程序、Desktop、Native 增加平台日期控件。
  9. 对已有数据库生成并审核升级计划。

12. 提交前检查

  1. 实体变化后运行 make:domainmake:localebuild 和项目要求的升级命令。
  2. 真实验证 Group 与 Todo 的新增、查询、更新、删除。
  3. 确认新增 Todo 的 groupId 正确,不只是界面显示在某个 tab 下。
  4. 运行目标平台构建,处理 XML、render props 和 Less Module 检查。
  5. 搜索并清除 anynever@ts-nocheck 和手写宽泛 props。
  6. 不提交数据库密码、token、local.properties、keystore 或本地数据库文件。

回到新手入门主线,或继续查看微信小程序DesktopNative的完整实现。