低代码接入外部数据能力:DataSource Runtime 的设计

低代码接入外部数据能力:DataSource Runtime 的设计

一、为什么需要 DataSource

在低代码系统中,从"页面渲染"到"应用构建",我目前的系统已经具备:

  • 动态数据绑定({{state.xxx}}
  • 生命周期执行(onMount)
  • 条件渲染 / 循环(condition / loop)

但有一个关键问题:页面的数据从哪里来?如何对接真实业务 API?

用 Lifecycle 当然能做:在 onMount 里手写 fetch、解析 JSON、调 ctx.set。但每个接口都要重复这套样板代码。

因此我引入了 DataSource :让 Schema 具备"声明式接入外部数据"的能力。Schema 只需要说"我要 /api/users 的数据",Runtime 负责剩下的一切。


二、DataSource 在低代码 Runtime 中的角色

在 Runtime 体系中,DataSource 扮演的是"外部数据输入层"。它与其他 Runtime 模块的关系如下:

css 复制代码
Runtime 能力层次
├── Context       → 数据容器(存储 state / data)
├── Expression    → 数据读取({{state.xxx}} / {{data.xxx}})
├── Lifecycle     → 副作用通道 A:执行自定义逻辑
└── DataSource    → 副作用通道 B:请求外部数据

Lifecycle 和 DataSource 是并行的两条副作用通道,各自独立地将结果写入 Context,最终都通过调度系统驱动 UI 更新。


三、DataSource 模型设计

当前是最小实现,我先建立了基础模型,设计如下:

Schema 协议

json 复制代码
{
  "dataSource": {
    "list": [
      {
        "id": "userList",
        "type": "fetch",
        "options": {
          "uri": "/api/list",
          "method": "GET"
        },
        "isInit": true
      }
    ]
  }
}

DataSourceItem 类型定义

typescript 复制代码
interface DataSourceItem {
  id: string              // 数据源标识,结果存入 ctx.data[id]
  type: 'fetch'           // V1 只支持 fetch
  options: {
    uri: string           // 请求地址
    method?: 'GET' | 'POST'  // 默认 GET
  }
  isInit?: boolean        // 是否组件挂载时自动请求,默认 false
}

interface DataSourceConfig {
  list: DataSourceItem[]
}

模型拆解

bash 复制代码
DataSourceItem
├── id        → 标识 + 结果存储位置(ctx.data[id])
├── type      → 请求类型(V1 仅 fetch)
├── options   → 请求配置(uri / method)
└── isInit    → 触发时机(true = 挂载时自动请求)

这是典型的:声明式(Schema) + 命令式(Runtime) 设计: Schema 只描述"数据是什么",Runtime 决定"如何获取数据"。


四、实现方案

4.1 整体架构

sequenceDiagram participant Schema as Schema (声明) participant RendererNode as RendererNodeItem participant PreviewRT as PreviewRuntime participant Handler as DataSourceHandler participant Ctx as RuntimeContext participant Root as RendererRoot Note over Schema,Root: 组件挂载阶段 RendererNode->>RendererNode: mount RendererNode->>PreviewRT: useEffect → runDataSource(ctx, schema) PreviewRT->>PreviewRT: 遍历 isInit=true 的 DataSourceItem loop 每个需要初始化的数据源 PreviewRT->>Handler: dataSourceHandler(uri, method) Handler-->>PreviewRT: Promise PreviewRT->>Ctx: ctx.set('data.' + id, data) end Ctx-->>Root: notifyUpward (向上冒泡) Root->>Root: rerender Root-->>RendererNode: UI 更新

4.2 请求处理器:DataSourceHandler

DataSource 的核心问题是"谁来发请求",解决方案是抽出 handler 接口,将"请求策略"与"请求时机"分离:

typescript 复制代码
// handler 接口:只关心"给我 uri 和 method,返回数据"
type DataSourceHandler = (
  options: { uri: string; method: string },
) => Promise<any>

// 默认实现:fetch
const defaultDataSourceHandler: DataSourceHandler = async (options) => {
  const res = await fetch(options.uri, { method: options.method })
  return res.json()
}

handler 通过 PreviewRuntime 构造函数注入:

typescript 复制代码
// 生产环境:使用fetch
const runtime = new PreviewRuntime({dataSourceHandler:defaultDataSourceHandler})

// 单元测试:注入 mock handler
const runtime = new PreviewRuntime({
  dataSourceHandler: async () => [{ name: 'Alice' }, { name: 'Bob' }]
})

这样 Runtime 只负责"何时调用 handler、结果存到哪",具体的请求实现由调用方在创建 Runtime 时注入。

4.3 执行阶段:runDataSource

PreviewRuntime 的 runDataSource 是纯执行逻辑:遍历 → 过滤 → 请求 → 存储。

typescript 复制代码
runDataSource(ctx: RuntimeContext, schema: Schema): void {
  const items = schema.dataSource?.list
  if (!items) return

  for (const item of items) {
    if (!item.isInit) continue
    this.dataSourceHandler({ uri: item.options.uri, method: item.options.method ?? 'GET' })
      .then(data => ctx.set(`data.${item.id}`, data))     // 结果存入 ctx.data[id]
      .catch(e => console.warn(`[DataSource] ${item.id} 请求失败:`, e))
  }
}

将结果按约定存入 ctx.data[id]。与 Lifecycle 存入 state 不同,DataSource 有自己专属的数据槽位 data,表达式引擎通过 {{data.userList}} 读取。

DesignerRuntime(设计态)空实现,通常通过 mock 数据或静态数据,不发请求:

typescript 复制代码
runDataSource(): void {}

4.4 接入 Renderer 层

与 Renderer 层现有 useEffect 的集成。RendererNodeItem 的 useEffect 同时触发 lifecycle 和 dataSource:

typescript 复制代码
useEffect(() => {
    runtime.runLifecycle('onMount', item.ctx, item.schema)
    runtime.runDataSource(item.ctx, item.schema)       // 触发 DataSource 执行
    return () => {
        runtime.runLifecycle('onUnmount', item.ctx, item.schema)
    }
}, [])

至此数据进入系统,形成完整闭环:fetch → ctx.set → UI 更新。

执行顺序:onMount(有异步任务)和 runDataSource 是并发执行的,不保证完成先后顺序,两者相互独立。


五、示例说明

如下 Schema,展示了一个从列表数据请求到 UI 更新的完整实现:

typescript 复制代码
const schema: Schema = {
  id: 'ds-demo',
  componentName: 'div',
  state: {},
  dataSource: {
    list: [
      {
        id: 'userList',
        type: 'fetch',
        options: { uri: 'https://jsonplaceholder.typicode.com/posts', method: 'GET' },
        isInit: true,
      },
    ],
  },
  children: [
    {
      id: 'user-item',
      componentName: 'p',
      loop: 'data.userList',
      loopArgs: ['user'],
      props: { children: '标题是:{{user.title}}' },
    },
  ],
}

执行流程:

kotlin 复制代码
1. 页面加载 → RendererNodeItem 挂载
2. useEffect 触发 → runtime.runDataSource(ctx, schema)
3. 遍历 dataSource → userList 的 isInit=true → 发起请求
4. handler({ uri: '.../posts', method: 'GET' }) → fetch → 返回 data
5. ctx.set('data.userList', data) → notifyUpward → RendererRoot rerender
6. loop 'data.userList' 展开 → 渲染 UI

六、关键决策

6.1 DataSourceHandler 通过构造函数注入

Runtime 内部不关心数据怎么拿(fetch / axios / mock),只关心"何时拿、存到哪"。handler 通过构造函数注入,遵循依赖注入原则:上层决定策略,Runtime 只负责执行。

所以 测试可以通过注入 mock handler,可以模拟任意接口返回,降低调试复杂性。

6.2 结果存 data 槽位而不是 state

RuntimeContext 有两个数据槽位:state(用户可变状态)和 data(外部数据)。DataSource 的结果约定存入 data

好处是职责清晰:state 是页面自己管理的状态(如 loading、visible),data 是外部注入的数据(如 API 返回的列表)。

两者来源不同、生命周期不同,分开存储让 Schema 结构一眼就能区分。

6.3 为什么不直接用 Lifecycle 做数据请求?

目前 DataSource 能做的事(请求接口,获取数据,绑定到data),Lifecycle 手写也能做到。那为什么还要单独建一套 DataSource 系统?

方案A:不引入 DataSource

所有数据请求都用 Lifecycle 的 onMount + methods 手写。
优点:

  • 系统更简单,只有一套副作用机制。

缺点:

  • 每个接口都要重复写 fetch → parse → ctx.set 的样板代码,Schema 体积膨胀
  • Runtime 无法识别哪些是数据请求,无法做统一的缓存、状态管理、错误处理。
方案B:引入 DataSource

把"请求外部接口"这个最高频场景抽成声明式配置。
缺点:

  • 系统多了1套类型(DataSourceItem)、1个接口方法(runDataSource)、1系列 handler

优点:

  • 只需要通过声明式方式在 Schema 里配置 uri 和 method等信息,简单通用
  • Runtime 统一管控请求行为,后续可以在 Runtime 层加缓存、加 loading、加重试,而不需要单独处理。

所以我最终选择引入 DataSource,因为数据请求是低代码场景中最普遍的副作用,用一层抽象来标准化是很有必要的。

它们二者的使用场景:DataSource 处理标准的接口请求,Lifecycle 处理需要自定义逻辑的场景。


七、总结

引入 DataSource 之前,页面的数据要么写死在 Schema 的 state 里,要么在 Lifecycle 里手写 fetch 逻辑。引入之后,Schema 只需要声明"我要哪个接口的数据",Runtime 自动完成请求、存储、更新的完整链路。

回顾整个系统的能力演进,每一层所解决的问题:

复制代码
Renderer    → 能显示       将 Schema 的 UI 结构渲染到页面
Expression  → 能计算       能绑定 {{state.count * price}} 动态数据到 UI
Lifecycle   → 能执行       在合适时机(onMount)触发自定义逻辑
DataSource  → 能接入数据    声明式方式对接外部 API

最终形成 Schema = UI 结构 + 数据绑定 + 行为逻辑 + 外部数据 ------ 一段 JSON 能描述一个完整页面


八、展望

当前 V1 版本的 DataSource 是基础版,可以简单配置 API 信息和 Data-id,Runtime 自动完成接口数据的绑定。

后续我会继续演进,比如:

能力增强方向

  • 支持options.params / headers / body
  • 请求状态的管理:ctx.data.loading / ctx.data.error
  • Plugin 扩展数据源类型:注册新的 DataSource type(如 WebSocket、GraphQL)

与 Runtime 深度结合

  • 页面交互 手动触发:ctx.reloadDataSource('userList')
  • URI 支持表达式:"/api/users/{{state.userId}}"
  • Lifecycle 可直接调用 DataSource

最终完成 DataSource 真正的"数据入口"职责,实现从数据到 UI 的全链路支持。

相关推荐
Jeking2174 小时前
低代码平台表单设计器 unione form editor 布局组件 —— 向导布局
低代码·动态表单·表单设计·表单引擎·unione cloud
踩着两条虫8 小时前
VTJ.PRO 开源 AI 低代码引擎深度评测大纲
前端·低代码·开源软件
Jeking2171 天前
低代码平台表单设计器 unione form editor 组件 —— 富文本编辑器
低代码·动态表单·表单设计·表单引擎·unione cloud
多租户观察室2 天前
中小微企业适用低代码开发平台有哪些选型
低代码
数睿数据无代码开发2 天前
2026 无代码平台企业选型推荐
低代码·无代码
咬人喵喵2 天前
E2编辑器里的零高容器是什么?怎么用?
低代码·微信·编辑器·交互·svg
Jeking2173 天前
低代码平台表单设计器 unione form editor 布局组件 — 折叠面板
低代码·动态表单·表单设计·表单引擎·unione cloud
低代码行业资讯3 天前
五大实锤证据:AI不会终结低代码,只会倒逼技术进化
低代码·ai
Teable任意门互动3 天前
深度解析:AI 赋能开源多维表格,实现企业全场景数据整合与高效应用
数据库·人工智能·低代码·信息可视化·开源·数据库开发