全部版块 我的主页
论坛 经济学论坛 三区 教育经济学
133 0
2025-12-04

本文由龚昱帆同学原创,基于TinyEngine运行时渲染解决方案的技术实践。

引言

在现代低代码开发中,运行时渲染器的作用日益重要。它能够在浏览器环境中直接解析并渲染低代码 Schema,提供一条无需生成源码即可即时运行的路径。这种机制使得开发者在设计阶段就能体验接近真实应用的交互效果与数据响应能力。

1. 启动流程与实例演示

以下通过一个简洁的示例页面,完整展示从 Schema 定义到运行时渲染的全过程。该页面包含三个基本元素:

  • 一段提示性文本;
  • 一个用于显示数值的按钮;
  • 点击按钮后计数自动递增的功能逻辑。

1.1 环境搭建

首先确保已获取包含 runtime-renderer 模块的新版本项目代码。

进入项目根目录后执行如下命令:

pnpm install

安装项目依赖项

pnpm run dev

启动前端服务

如需前后端联调,可参考相关文档或视频教程启动 Java 后端服务,以获得更完整的开发调试体验。

1.2 页面 Schema 配置

  1. 创建新页面
    DemoA
    并添加页面状态定义
    state1
  2. 在画布中拖入 Text 组件和 TinyButton 组件:
    • 设置 Text 的内容为:“[state测试]:点击增加button计数”;
    • TinyButton 的
      text
      绑定表达式
      this.state.state1.button
    • TinyButton 的
      onClick
      绑定表达式
      this.onClickNew1
  3. 在“页面 JS”区域中定义处理方法
    onClickNew1

1.3 运行时渲染执行流程

当用户点击“运行时渲染”按钮,或直接访问 runtime 页面时,系统将触发以下流程:

runtime-renderer 将按步骤处理请求:

  1. 解析当前 URL 参数,提取 appId、tenant 及路由信息。若正处于某页面编辑状态,则自动跳转至对应页面。系统依据页面树节点中的 route 字段,沿祖先链拼接出完整路径
    #/<a>/<b>/<c>
    。例如生成链接:
    http://localhost:8090/runtime.html?id=1&tenant=1&platform=1#/demoa
    。若设计器内有更新内容,需重新加载运行时页面以同步最新 Schema。
  2. 通过 useAppSchema 获取 App 层级的 Schema 数据,并初始化应用配置。
  3. 定位到当前需要渲染的页面对应的 Schema 结构
    DemoA
    和其具体数据模型
    page_content
  4. RenderMain 使用该
    page_content
    构建页面上下文环境,包括:
    • 初始化页面 state 状态;
    • 解析并注入页面级方法
      onClickNew1
    • 注入页面专属的 CSS 作用域。
  5. 调用渲染引擎,根据 Schema 递归构建 VNode 树:
    • Text 节点直接输出静态文本内容;
    • TinyButton 节点则进行复杂处理:
      • 解析
        text
        中的 JSExpression,读取
        this.state.state1.button
        ,初始值设为 1;
      • 解析
        onClick
        中的 JSExpression,将其转换为对
        onClickNew1
        函数的引用。
  6. Vue 框架将生成的 VNode 树挂载至 DOM,最终呈现给用户的页面中,按钮上显示数字“1”。

当用户触发按钮点击事件时:

  • 绑定在
    onClick
    上的方法
    onClickNew1
    被调用;
  • 该函数在当前页面上下文中执行
    this.state.state1.button++
    操作;
  • Vue 的响应式系统检测到 state 发生变化,驱动 TinyButton 文本重新渲染;
  • 按钮上的数值随之从 1 逐步更新为 2、3、4……

2. 技术原理概述

TinyEngine 将页面的结构、样式及交互逻辑统一描述为 JSON 格式的 Schema。可视化设计器允许开发者通过图形界面编辑这些 Schema,而最终交付给浏览器的是由代码生成或运行时动态渲染出的 Vue 应用。

runtime-renderer 的核心目标是在浏览器端直接将 Schema 渲染为可交互的 Vue 实例,从而形成一条与传统“出码”方式并行的“即时运行”通道:

  • 同一份 Schema 同时服务于设计画布、运行时渲染与代码生成;
  • 支持应用级别的全局配置,如物料包管理、国际化(i18n)、数据源接入、工具函数注册等;
  • 完整实现区块复用、循环渲染、条件判断、插槽机制、状态管理和事件函数调用等高级功能。

3. 整体架构设计:从 App Schema 到可视页面

从宏观视角来看,runtime-renderer 的主流程可以归纳为:

URL 参数(appId)
 ↓
 加载 App Schema 和页面列表
 ↓
 初始化应用级环境(物料 / i18n / 数据源 / utils / 全局 CSS)
 ↓
 根据 pageId 选中页面 pageSchema
 ↓
 RenderMain 构建页面上下文并解析 state / methods
 ↓
 renderer 按 Schema 递归生成 Vue VNode 树
 ↓
 Vue 挂载到真实 DOM

3.1 模块职责划分

系统按照功能职责划分为多个模块,协同完成渲染任务:

useAppSchema

  • 负责拉取整个应用的 Schema 数据(含应用元信息与页面列表);
  • 完成物料包、依赖项、数据源、工具函数、i18n 配置以及全局 CSS 的初始化;
  • 对外暴露接口,支持查询页面列表或按 ID 获取特定 pageSchema。

app-function 相关模块

  • 封装通用能力,包括物料包加载、importMap 处理、数据源初始化、工具函数准备等;
  • 提供统一的查询接口,如
    getDataSource()
    getUtilsAll()
    等。

RenderMain + PageRenderer

  • PageRenderer 作为对外暴露的高层组件,使用者仅需传入
    pageId
    即可;
  • RenderMain 承担核心上下文构建职责:
    • 基于传入的 Schema 初始化页面运行环境;
    • 组织状态、方法和样式的注入流程;
    • 协调各子模块完成渲染前准备。
pageId

选择当前页面的
pageSchema


构建该页面的上下文环境,包括 state、route、router、stores、dataSourceMap、utils 和 cssScopeId 等信息;
解析页面中定义的 methods 与 state 数据结构;
调用 renderer 模块完成页面的实际渲染流程。

renderer(render.ts)

作为核心渲染模块,负责将 schema 节点转换为真实的组件 VNode。
主要处理内容包括:组件映射解析、属性绑定、循环渲染、条件判断、插槽机制、区块管理以及 CSS 作用域隔离等逻辑。

parser(parser.ts)

该模块为配置解析引擎,统一处理 JSExpression、JSFunction、i18n 文案及插槽等配置形式,将其转化为可在运行时使用的值或可执行函数。

page-function 系列

提供页面级别的状态管理(state)、CSS 作用域控制、Block 上下文支持等功能,保障页面逻辑的独立性和可维护性。

3.2 三层上下文机制

为了确保表达式和函数在执行时具备完整的环境信息,runtime-renderer 设计了三层上下文结构:

应用级上下文:包含物料组件库、数据源集合
dataSourceMap
、国际化配置、工具函数(utils)、全局 CSS 样式、路由实例 router、状态存储 stores 等共享资源。

页面级上下文:涵盖当前页面的 state 状态、路由信息、页面级 CSS Scope Id、methods 方法集合以及生命周期相关配置。

区块级上下文:用于管理独立区块内的 state 与 CSS 作用域,通过特定机制生成
getBlockContext
/
getBlockCssScopeId


所有 JSExpression、JSFunction 及插槽函数均在“局部作用域(如循环变量)→ 页面/区块上下文 → 应用级上下文”的链式作用域环境中进行求值与执行。

4. 详细设计说明

4.1 应用级初始化流程

当运行时入口加载完毕后,启动应用级初始化,主要包括以下步骤:

4.1.1 加载完整应用 Schema

runtime-renderer 通过两个接口获取完整的应用配置:

/app-center/v1/api/apps/schema/:appId
:返回应用元数据,包括全局变量
globalState
、物料包信息
packages
、组件映射关系
componentsMap
、数据源定义
dataSource
、国际化文案
i18n
、工具函数列表
utils
、全局 CSS 配置等。

/app-center/api/pages/list/:appId
:返回页面清单,每个条目包含路由路径、页面标题,以及设计器保存的页面结构数据
page_content


useAppSchema 模块将上述两部分数据聚合,构建成内存中的完整 App Schema,后续所有页面渲染均以此为基础。

4.1.2 物料与依赖初始化

物料系统的初始化分为两个层级:

1) 基础物料包(bundle.json)加载
useAppSchema
优先从
/mock/bundle.json
中读取
data.materials.packages
,获取一组预设的基础物料包配置;
这些包通常是 TinyEngine 内置的常用组件库(如 TinyVue),作为运行环境的基础依赖优先加载;
loadPackageDependencys(packages)
负责根据配置动态引入对应的 JS 与 CSS 资源。

2) 按组件映射加载具体物料组件
依据 App Schema 中的
componentsMap

packages
,runtime-renderer 生成详细的组件依赖描述,包括:
- 组件所属的 npm 包名;
- 导出方式(默认或具名),是否需要解构;
- 所需的 JS 与 CSS 资源列表。

随后通过
getComponents
逐个拉取组件实现,并配合
addStyle
注入相应的样式文件。

整体流程遵循:先依据 bundle.json 加载基础物料包,再根据 componentsMap 精细化加载所需组件。
加载完成后,组件实现会被注册到全局对象(如
window.TinyLowcodeComponent
/
window.TinyComponentLibs
),以便在渲染阶段通过组件名称快速查找对应实现。

4.1.3 importMap 与第三方依赖初始化

针对在 /mock/bundle.json 中声明的包所依赖的子模块,以及其他通过 CDN 引入的第三方库,runtime-renderer 使用 importMap 进行统一映射管理:


import-map.json
中维护包名到实际 CDN 地址的映射关系;
在应用启动时,将 importMap 注入浏览器运行环境,使得动态导入的模块可以直接使用包名进行引用。

4.1.4 国际化配置初始化

App Schema 中的
i18n
字段包含多语言文案数据:

运行时遍历各个 locale 下的文案条目;
将其合并至国际化实例(例如
i18n.global
)中;
parser 在解析表达式时,若检测到
this.i18n

t(
的调用,会自动将翻译函数注入当前执行上下文。

4.1.5 工具函数初始化

工具函数
utils
以配置形式存在于 App Schema 中,目前支持两种来源:

1) NPM 包工具函数(type: 'npm')
在 Schema 中定义包名、版本号、导出名称、是否需解构、子字段路径
subName
等信息;
运行时通过 CDN(如
https://unpkg.com/<package>@<version>
)动态
import
对应 NPM 包;
根据配置提取默认导出或具名导出的内容,供后续调用使用。

通过引入第三方 NPM 包作为工具函数,可以在不修改运行时代码的基础上扩展功能能力。

函数型工具函数(type: 'function')

此类工具函数以 JSFunction 的形式直接定义在 Schema 中。运行时会通过以下方式处理:

  • 利用
    parseJSFunction
    将其解析为实际可执行的函数,并进行缓存;
  • 所有解析后的工具函数将被统一挂载至一个共享的工具集合中,并通过
    getUtilsAll()
    对外暴露;
  • 页面上下文通过
    utils
    注入该集合,使得表达式和方法能够通过
    this.utils.xxx
    调用相应工具函数。

4.1.6 数据源初始化

数据源配置信息由

dataSource
提供,描述了应用中可用的本地或远程数据源。初始化阶段将执行以下操作:

  • 将每个数据源封装为可直接调用的对象;
  • 统一挂载到
    dataSourceMap
    下,例如形成类似
    this.dataSourceMap.tableTest1.load(params)
    的结构;
  • 按照设计器约定的 dataHandler 规则,对后端返回的数据结构进行标准化处理,尽可能统一为如
    { items, total }
    所示的通用格式,便于表格等组件直接使用。

页面级函数及生命周期逻辑可通过

this.dataSourceMap
访问并使用这些已初始化的数据源。

4.1.7 区块 Schema 加载

区块(Block)是可复用的页面片段,runtime-renderer 会通过

/material-center/api/blocks
获取区块列表,并进行如下处理:

  • 按 label 将区块组织成映射关系,例如生成
    window.blocks['Group1Test1'] = { schema, meta }
    结构;
  • 在渲染过程中,若发现某个节点的
    componentName
    与某一区块 label 匹配,则将其视为 Block 组件进行处理:
    • 采用该区块自身的 schema;
    • 创建独立的 Block 上下文与 CSS Scope;
    • 递归渲染其 children 内容。

4.1.8 全局变量初始化

运行时的全局状态管理基于 Pinia 实现,即 stores。具体流程如下:

  • 在启动入口
    initRuntimeRenderer
    中,首先调用
    generateStoresConfig()
    ,根据 App Schema 中的全局状态配置生成标准的 stores 配置;
  • 创建 Pinia 实例,并通过
    createStores(storesConfig, pinia)
    将上述配置注册为实际的 store;
  • 将生成的
    stores
    对象通过
    app.provide('stores', stores)
    注入整个应用,使页面组件可通过依赖注入获取;
  • RenderMain 在构建页面上下文时,会将此
    stores
    注入 context,表达式和方法即可通过
    this.stores.xxx
    访问对应 store。

由此,设计器可通过配置声明全局状态切片,而运行时统一依托 Pinia 实现,享受其响应式机制与开发者工具生态支持。

4.1.9 路由系统初始化(vue-router)

runtime-renderer 使用

vue-router
管理页面级导航。初始化过程包括:

  • createAppRouter
    中,从
    useAppSchema().pages
    读取所有页面配置;
  • 依据每个页面的
    route
    id
    parentId
    isHome
    isDefault
    等字段生成路由表;
  • 每条页面记录转化为一条路由项
    route
    • path
      来源于
      page.route
    • component
      统一指向惰性加载的
      PageRenderer
      ,并通过
      props: { pageId: page.id }
      传递页面 id;
  • 利用
    parentId
    构建嵌套路由结构;
  • 根据
    isDefault
    在父级路由上设置默认子路由重定向;
  • 根据
    isHome
    添加从
    /
    到首页的重定向规则;
  • 最终基于动态生成的
    routes
    调用
    createRouter({ history: createWebHashHistory('/runtime.html'), routes })
    创建 router 实例;
  • 启动入口
    initRuntimeRenderer
    将该 router 挂载至应用,实现通过 hash 路由切换页面。

4.2 页面级渲染入口

页面渲染的核心由两个组件构成:对外暴露的

PageRenderer
和实际执行渲染的
RenderMain

4.2.1 PageRenderer:对外接口封装

使用者仅需:

  • 引入
    <PageRenderer :pageId="currentPageId" />
  • PageRenderer 内部将
    pageId
    透传给 RenderMain,隐藏所有关于 Schema 解析与上下文构建的技术细节。
4.2.2 根据 pageId 获取 pageSchema

RenderMain 在

setup
中完成以下步骤:

  • 通过
    useAppSchema().getPageById(pageId)
    查找对应的页面对象;
  • 提取其中的
    page_content
    作为当前页面的 schema;
  • 使用
    computed
    进行包装,确保后续更新可被侦测;
  • page_content
    执行深拷贝,防止渲染过程中意外修改原始数据。

随后通过

watch
监听当前 schema 变化:

  • 首次进入页面时立即触发
    setSchema
    完成初始化;
  • 当设计器更新页面并同步至运行时,再次触发
    setSchema
    ,实现设计态与运行态的实时联动。
4.2.3 页面上下文构建

setSchema
是 RenderMain 的核心逻辑之一,负责基于当前 pageSchema 构建完整的页面级上下文,主要包括:

  • 从路由系统获取当前路径信息
    route
    与参数
    router
  • 通过依赖注入获取全局状态容器
    stores
  • 通过 app-function 模块获取
    dataSourceMap

使用相关配置初始化页面级上下文环境,并生成当前页面的唯一标识符,例如用于区分不同页面实例。这些信息会被整合后,在运行时上下文初始化阶段注入到执行环境中。

通过调用特定方法清空之前的上下文内容,防止在页面切换或 Schema 更新过程中残留旧状态数据。后续对 methods 和 state 的解析操作都将在这一全新的上下文中进行,确保执行环境的纯净与一致。

utils

4.2.4 方法与状态的初始化流程

在上下文环境中,初始化过程遵循明确的顺序,以保障依赖关系正确:

  1. 构建执行上下文:首先调用初始化函数,确保诸如页面实例、工具模块、状态管理器以及公共方法等核心对象可在后续解析中被正常访问。
  2. 处理 methods 配置:遍历 schema 中定义的方法集合,逐项进行解析:
    • 将 JSFunction 字符串转换为可执行的真实函数;
    • 使用运行时包装器对其进行封装,使其具备完整的上下文绑定能力及异常捕获机制;
    • 将处理后的函数存入方法容器,并合并至当前 context 中供调用。
  3. 初始化 state 状态:执行状态初始化逻辑:
    • 依据 defaultValue 设置初始值;
    • 对含有 getter/setter 的字段记录其访问器行为;
    • 由于 state 中的表达式常依赖 props、utils、stores 或 methods,因此必须在 methods 解析完成后执行此步骤。
  4. 注入页面级样式:调用样式注入函数:
    • 为当前页面添加带有特定前缀的 CSS 类名;
    • 渲染器在创建节点时会自动附加该属性,从而实现组件样式的隔离。

该顺序设计有效避免了“解析过程中无法访问所需依赖”的问题,保证上下文完整性。

useState
state
setState
cssScopeId
data-te-page-<pageId>
contextData
setSchema
setContext(contextData, true)
true
this.state
this.stores
this.dataSourceMap
this.utils
methods
parseData
generateFn
methods
setState(newSchema.state, true)
setPageCss(pageSchema.css, cssScopeId)
[data-te-page-<id>]

4.2.5 渲染主函数中的根容器结构

RenderMain 的渲染函数不会直接将页面 schema 传递给 renderer,而是先构造一个统一的根容器对象:

const rootChildrenSchema = {
  componentName: 'div',
  props: { ...(pageSchema.props || {}) },
  children: pageSchema.children
}

此举旨在保持与“出码”结构的一致性,同时便于集中挂载页面级别的样式和属性。

当 children 内容存在时,执行渲染:

h(renderer, { schema: rootChildrenSchema, parent: pageSchema })

若 children 为空,则渲染一个默认占位组件,防止页面呈现完全空白的状态。

render
pageSchema.children
pageSchema.children
Loading

4.3 核心渲染机制:从 Schema 到 VNode

renderer 模块负责将 Schema 节点转化为 Vue 的 VNode 结构,而 parser 则负责将各类配置项解析为运行时可用的实际值,两者协作完成整体渲染流程。

4.3.1 组件类型的解析优先级

根据节点的 componentName 字段,renderer 按照以下优先级查找对应实现:

  1. 匹配内置的 Canvas 系列组件(如布局容器、画布元素等);
  2. 查找运行时加载的 TinyVue 组件,或已在注册表中声明的物料组件;
  3. 支持自定义元素(Web Components),通过预设的映射表提供扩展能力;
  4. 识别原生 HTML 标签:若 componentName 是合法的 HTML 标签名,则直接作为 DOM 元素处理;
  5. 尝试匹配区块组件(block):
    • 若在 block 注册表中找到同名定义,则动态创建一个 Vue 组件;
    • 在组件内部基于 block 的 schema 及其上下文递归渲染子节点;
    • 应用独立的 CSS Scope Id 实现样式隔离。

若所有匹配均失败,则使用默认占位组件(如 CanvasPlaceholder)进行兜底渲染,确保单个节点错误不会导致整个页面渲染中断。

componentName
Text
Img
RouterLink
Collection
window.TinyLowcodeComponent
customElements
window.blocks
4.3.2 属性解析与样式作用域控制

Schema 中的 props 字段可能包含多种类型的数据:基础值、JSExpression、JSFunction、状态访问器、图标配置、插槽声明等。renderer 使用统一解析器对这些属性进行处理,最终生成标准化的 props 对象:

  • JSExpression:在当前作用域与上下文中执行表达式,获取计算结果;
  • JSFunction:解析为实际函数并绑定执行上下文;
  • 状态访问器:根据默认逻辑或 getter 规则进行求值;
  • 插槽声明:依据配置生成对应的 Slot 函数;
  • 复杂对象与数组:递归调用解析函数进行深度处理。

在此基础上,renderer 还会结合 scope 或 context 中的页面标识符,为非 Block 类型的组件自动添加形如指定格式的属性标记,配合渲染机制实现细粒度的样式隔离。

props
parseData
parseData
cssScopeId
[data-te-page-xxx]: ''

用于样式作用域隔离的属性处理;

为 Canvas 和 Block 组件增加特定字段挂载,

便于组件内部依据 Schema 实现渲染逻辑;

将原有命名进行调整并重命名为新标识,

class

以避免对组件内部默认样式约定造成覆盖。

4.3.3 条件、循环与作用域管理

通过以下三个字段实现条件判断与列表循环的描述:

loop

通常为 JSExpression 类型,返回一个数组作为遍历源;

loopArgs

定义 item 与 index 在表达式中的变量名,例如:

['row', 'i']

以及

condition

该字段为 JSExpression,用于控制节点是否需要被渲染。

renderer 的执行流程如下:

  1. 利用
  2. parseData(loop, scope, context)
  3. 获取循环所需的数据数组;
  4. 针对每个数据项,调用
  5. parseLoopArgs
  6. 生成局部作用域(如
  7. { row, i }
  8. );
  9. 将局部作用域合并至当前上下文,形成新的作用域环境
  10. mergeScope
  11. 使用
  12. parseCondition(condition, mergeScope, context)
  13. 判断当前项是否应被渲染;
  14. 在扩展后的作用域
  15. mergeScope
  16. 下解析子节点与属性,生成对应的 VNode 结构。

若未配置 loop 字段,则直接在当前作用域中渲染一次该节点即可。

4.3.4 子节点与插槽机制

children 的处理根据不同的结构情况采取相应策略:

当组件被标记为容器且 children 为空时,系统会自动注入默认内容,

CanvasPlaceholder

从而提升设计阶段的操作体验与调试便利性。

若 children 非数组形式且本身为表达式,则直接调用解析器进行求值,

parseData(children, scope, context)

适用于 Text 或简单插值等场景。

对于普通数组类型的 children,且不包含 Template 节点的情况,

renderGroup

采用递归方式逐个渲染各个子元素。

如果 children 中存在

componentName: 'Template'

则进行如下处理:

  • 使用
  • generateSlotGroup
  • 按照 slotName 对节点进行分组;
  • 为每个插槽生成类似
  • ($scope) => renderDefault(children, { ...scope, ...$scope })
  • 的函数封装;
  • 在创建组件 VNode 时,将其作为 slots 参数传入,实现命名插槽功能。

对于 Web Components 场景,renderer 会在必要时自动为子节点添加合适的属性,

slot

以符合自定义元素的插槽规范要求。

4.3.5 parser 的核心职责

parser 充当“多类型配置解析器”的角色,借助一张规则映射表,

将多种格式的数据统一转换为运行时可用的值。

它通过不同的识别函数判断数据类型,包括:

type(data)

支持 JSExpression、JSFunction、JSSlot、i18n、状态访问器、Icon、字符串、数组、对象等。

每种类型对应一个专门的解析处理器

parseFunc(data, scope, ctx)

负责实现具体的转换逻辑。

统一入口函数

parseData(data, scope, ctx)

根据首个匹配成功的类型选择对应的解析方法。

在 renderer 解析 props、children、loop、condition 等字段时,均会调用

parseData

从而在无需了解配置细节的前提下,准确获取运行时结果。

目前数据源和 Collection 组件在 Schema 层并未进行 parser 级别的特殊处理,

其解析行为与普通组件保持一致。相关功能主要依赖上下文中提供的

dataSourceMap

以及组件自身的协议约定来完成。

5. 总结

runtime-renderer 将原本仅能在代码生成阶段完成的 “Schema → 运行应用” 流程迁移至浏览器端执行,具体包括:

  • 通过 useAppSchema 拉取并初始化 App Schema,构建应用级别的运行环境;
  • 借助 RenderMain 建立页面级上下文,统一管理 state、methods、路由、数据源及样式资源;
  • 结合 renderer 与 parser,将 Schema 节点递归转化为 Vue 的 VNode,并在多层级作用域中安全执行表达式与函数调用。

对于可视化设计器用户而言,这一机制提供了一条真正意义上的“所见即所得”运行路径。

schema

className

loop

loopArgs

condition

二维码

扫码加我 拉你入群

请注明:姓名-公司-职位

以便审核进群资格,未注明则拒绝

栏目导航
热门文章
推荐文章

说点什么

分享

扫码加好友,拉您进群
各岗位、行业、专业交流群