低代码接入外部数据能力: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<data> 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 的全链路支持。

相关推荐
卷叶小树2 天前
Style Resolver:Schema 驱动低代码平台的样式能力设计
低代码
SL_staff10 天前
3周搭完MES系统:JVS低代码+JVS-IoT物联网的实战记录
java·前端·低代码
AprChell12 天前
低代码设计器和低代码设计引擎架构综述
前端·vue.js·低代码
Kagol16 天前
NocoBase 开源项目源码深度分析
低代码
UXbot18 天前
帮助企业低门槛开展AI应用开发的平台推荐
前端·低代码·ui·交互·产品经理·原型模式·web app
盟接之桥18 天前
电子数据交换(EDI)|制造业汽车零配件场景方案
大数据·网络·人工智能·安全·低代码·汽车·制造
UXbot19 天前
如何选择适合公司项目的UI设计工具?企业选型指南
前端·低代码·ui·团队开发·原型模式·设计规范·web app
UXbot19 天前
原型设计工具如何帮助新人快速进入产品行业?
前端·低代码·ui·交互·团队开发·原型模式·web app
NocoBase19 天前
程序员和软件还有前途吗 —— 从 NocoBase 收入再翻倍谈起
低代码·ai·开源·无代码·管理工具·内部工具·无代码开发平台
盟接之桥19 天前
制造业汽车零配件EDI软件场景方案
网络·安全·低代码·汽车·制造