本文由龚昱帆同学原创,基于TinyEngine运行时渲染解决方案的技术实践。
在现代低代码开发中,运行时渲染器的作用日益重要。它能够在浏览器环境中直接解析并渲染低代码 Schema,提供一条无需生成源码即可即时运行的路径。这种机制使得开发者在设计阶段就能体验接近真实应用的交互效果与数据响应能力。
以下通过一个简洁的示例页面,完整展示从 Schema 定义到运行时渲染的全过程。该页面包含三个基本元素:
首先确保已获取包含 runtime-renderer 模块的新版本项目代码。
进入项目根目录后执行如下命令:
pnpm install
安装项目依赖项
pnpm run dev
启动前端服务
如需前后端联调,可参考相关文档或视频教程启动 Java 后端服务,以获得更完整的开发调试体验。
DemoA
并添加页面状态定义
state1
:
text
绑定表达式
this.state.state1.button
;
onClick
绑定表达式
this.onClickNew1
。
onClickNew1
:
当用户点击“运行时渲染”按钮,或直接访问 runtime 页面时,系统将触发以下流程:
runtime-renderer 将按步骤处理请求:
route 字段,沿祖先链拼接出完整路径
#/<a>/<b>/<c>
。例如生成链接:
http://localhost:8090/runtime.html?id=1&tenant=1&platform=1#/demoa
。若设计器内有更新内容,需重新加载运行时页面以同步最新 Schema。
useAppSchema 获取 App 层级的 Schema 数据,并初始化应用配置。DemoA
和其具体数据模型
page_content
。RenderMain 使用该
page_content
构建页面上下文环境,包括:
onClickNew1
;text
中的 JSExpression,读取
this.state.state1.button
,初始值设为 1;onClick
中的 JSExpression,将其转换为对
onClickNew1
函数的引用。当用户触发按钮点击事件时:
onClick
上的方法
onClickNew1
被调用;this.state.state1.button++
操作;
TinyEngine 将页面的结构、样式及交互逻辑统一描述为 JSON 格式的 Schema。可视化设计器允许开发者通过图形界面编辑这些 Schema,而最终交付给浏览器的是由代码生成或运行时动态渲染出的 Vue 应用。
runtime-renderer 的核心目标是在浏览器端直接将 Schema 渲染为可交互的 Vue 实例,从而形成一条与传统“出码”方式并行的“即时运行”通道:
从宏观视角来看,runtime-renderer 的主流程可以归纳为:
URL 参数(appId)
↓
加载 App Schema 和页面列表
↓
初始化应用级环境(物料 / i18n / 数据源 / utils / 全局 CSS)
↓
根据 pageId 选中页面 pageSchema
↓
RenderMain 构建页面上下文并解析 state / methods
↓
renderer 按 Schema 递归生成 Vue VNode 树
↓
Vue 挂载到真实 DOM
系统按照功能职责划分为多个模块,协同完成渲染任务:
useAppSchema
app-function 相关模块
getDataSource()
和
getUtilsAll()
等。RenderMain + PageRenderer
PageRenderer 作为对外暴露的高层组件,使用者仅需传入
pageId
即可;RenderMain 承担核心上下文构建职责:pageIdpageSchema;dataSourceMap、国际化配置、工具函数(utils)、全局 CSS 样式、路由实例 router、状态存储 stores 等共享资源。getBlockContext/getBlockCssScopeId。/app-center/v1/api/apps/schema/:appId:返回应用元数据,包括全局变量globalState、物料包信息packages、组件映射关系componentsMap、数据源定义dataSource、国际化文案i18n、工具函数列表utils、全局 CSS 配置等。/app-center/api/pages/list/:appId:返回页面清单,每个条目包含路由路径、页面标题,以及设计器保存的页面结构数据page_content。useAppSchema 优先从/mock/bundle.json 中读取data.materials.packages,获取一组预设的基础物料包配置;loadPackageDependencys(packages) 负责根据配置动态引入对应的 JS 与 CSS 资源。componentsMap 与packages,runtime-renderer 生成详细的组件依赖描述,包括:getComponents 逐个拉取组件实现,并配合addStyle 注入相应的样式文件。window.TinyLowcodeComponent/window.TinyComponentLibs),以便在渲染阶段通过组件名称快速查找对应实现。import-map.json 中维护包名到实际 CDN 地址的映射关系;i18n 字段包含多语言文案数据:i18n.global)中;this.i18n 或t( 的调用,会自动将翻译函数注入当前执行上下文。utils 以配置形式存在于 App Schema 中,目前支持两种来源:subName 等信息;https://unpkg.com/<package>@<version>)动态import 对应 NPM 包;通过引入第三方 NPM 包作为工具函数,可以在不修改运行时代码的基础上扩展功能能力。
此类工具函数以 JSFunction 的形式直接定义在 Schema 中。运行时会通过以下方式处理:
parseJSFunction 将其解析为实际可执行的函数,并进行缓存;getUtilsAll() 对外暴露;utils 注入该集合,使得表达式和方法能够通过 this.utils.xxx 调用相应工具函数。数据源配置信息由
提供,描述了应用中可用的本地或远程数据源。初始化阶段将执行以下操作:dataSource
dataSourceMap 下,例如形成类似 this.dataSourceMap.tableTest1.load(params) 的结构;{ items, total } 所示的通用格式,便于表格等组件直接使用。页面级函数及生命周期逻辑可通过
访问并使用这些已初始化的数据源。this.dataSourceMap
区块(Block)是可复用的页面片段,runtime-renderer 会通过
获取区块列表,并进行如下处理:/material-center/api/blocks
window.blocks['Group1Test1'] = { schema, meta } 结构;componentName 与某一区块 label 匹配,则将其视为 Block 组件进行处理:运行时的全局状态管理基于 Pinia 实现,即 stores。具体流程如下:
initRuntimeRenderer 中,首先调用 generateStoresConfig(),根据 App Schema 中的全局状态配置生成标准的 stores 配置;createStores(storesConfig, pinia) 将上述配置注册为实际的 store;stores 对象通过 app.provide('stores', stores) 注入整个应用,使页面组件可通过依赖注入获取;stores 注入 context,表达式和方法即可通过 this.stores.xxx 访问对应 store。由此,设计器可通过配置声明全局状态切片,而运行时统一依托 Pinia 实现,享受其响应式机制与开发者工具生态支持。
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 路由切换页面。页面渲染的核心由两个组件构成:对外暴露的
和实际执行渲染的 PageRenderer
。RenderMain
使用者仅需:
<PageRenderer :pageId="currentPageId" />;pageId 透传给 RenderMain,隐藏所有关于 Schema 解析与上下文构建的技术细节。RenderMain 在
中完成以下步骤:setup
useAppSchema().getPageById(pageId) 查找对应的页面对象;page_content 作为当前页面的 schema;computed 进行包装,确保后续更新可被侦测;page_content 执行深拷贝,防止渲染过程中意外修改原始数据。随后通过
监听当前 schema 变化:watch
setSchema 完成初始化;setSchema,实现设计态与运行态的实时联动。
是 RenderMain 的核心逻辑之一,负责基于当前 pageSchema 构建完整的页面级上下文,主要包括:setSchema
route 与参数 router;stores;dataSourceMap 和使用相关配置初始化页面级上下文环境,并生成当前页面的唯一标识符,例如用于区分不同页面实例。这些信息会被整合后,在运行时上下文初始化阶段注入到执行环境中。
通过调用特定方法清空之前的上下文内容,防止在页面切换或 Schema 更新过程中残留旧状态数据。后续对 methods 和 state 的解析操作都将在这一全新的上下文中进行,确保执行环境的纯净与一致。
utils
在上下文环境中,初始化过程遵循明确的顺序,以保障依赖关系正确:
该顺序设计有效避免了“解析过程中无法访问所需依赖”的问题,保证上下文完整性。
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>]
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
renderer 模块负责将 Schema 节点转化为 Vue 的 VNode 结构,而 parser 则负责将各类配置项解析为运行时可用的实际值,两者协作完成整体渲染流程。
根据节点的 componentName 字段,renderer 按照以下优先级查找对应实现:
若所有匹配均失败,则使用默认占位组件(如 CanvasPlaceholder)进行兜底渲染,确保单个节点错误不会导致整个页面渲染中断。
componentName
Text
Img
RouterLink
Collection
window.TinyLowcodeComponent
customElements
window.blocks
Schema 中的 props 字段可能包含多种类型的数据:基础值、JSExpression、JSFunction、状态访问器、图标配置、插槽声明等。renderer 使用统一解析器对这些属性进行处理,最终生成标准化的 props 对象:
在此基础上,renderer 还会结合 scope 或 context 中的页面标识符,为非 Block 类型的组件自动添加形如指定格式的属性标记,配合渲染机制实现细粒度的样式隔离。
props
parseData
parseData
cssScopeId
[data-te-page-xxx]: ''用于样式作用域隔离的属性处理;
为 Canvas 和 Block 组件增加特定字段挂载,
便于组件内部依据 Schema 实现渲染逻辑;
将原有命名进行调整并重命名为新标识,
class
以避免对组件内部默认样式约定造成覆盖。
通过以下三个字段实现条件判断与列表循环的描述:
loop
通常为 JSExpression 类型,返回一个数组作为遍历源;
loopArgs
定义 item 与 index 在表达式中的变量名,例如:
['row', 'i']
以及
condition
该字段为 JSExpression,用于控制节点是否需要被渲染。
renderer 的执行流程如下:
parseData(loop, scope, context)
parseLoopArgs
{ row, i }
mergeScope
parseCondition(condition, mergeScope, context)
mergeScope
若未配置 loop 字段,则直接在当前作用域中渲染一次该节点即可。
children 的处理根据不同的结构情况采取相应策略:
当组件被标记为容器且 children 为空时,系统会自动注入默认内容,
CanvasPlaceholder
从而提升设计阶段的操作体验与调试便利性。
若 children 非数组形式且本身为表达式,则直接调用解析器进行求值,
parseData(children, scope, context)
适用于 Text 或简单插值等场景。
对于普通数组类型的 children,且不包含 Template 节点的情况,
renderGroup
采用递归方式逐个渲染各个子元素。
如果 children 中存在
componentName: 'Template'
则进行如下处理:
generateSlotGroup
($scope) => renderDefault(children, { ...scope, ...$scope })
对于 Web Components 场景,renderer 会在必要时自动为子节点添加合适的属性,
slot
以符合自定义元素的插槽规范要求。
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
以及组件自身的协议约定来完成。
runtime-renderer 将原本仅能在代码生成阶段完成的 “Schema → 运行应用” 流程迁移至浏览器端执行,具体包括:
对于可视化设计器用户而言,这一机制提供了一条真正意义上的“所见即所得”运行路径。
schema
className
loop
loopArgs
condition
扫码加好友,拉您进群



收藏
