前言
运行时渲染器用于在浏览器中直接渲染低代码 Schema,提供与"出码"并行的即时运行路径,可在设计阶段获得接近真实的交互与数据效果。
1.启动流程与案例讲解
下面用一个非常简单的示例页面,串联起从 Schema 到运行时渲染的完整流程。这个页面包含:
- 一段提示文案;
- 一个显示计数的按钮;
- 点击按钮时,计数加一。
1.1 环境准备
- 确保已拉取包含 runtime-renderer 包的新版本代码。
- 在项目根目录执行:
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 , 如果需要设计器内内容有更新的话则需要重新加载运行时页面以同步。 2). 通过 useAppSchema 拉取 App Schema,初始化应用配置。 3). 并找到 DemoA 对应的 page_content。 4). RenderMain 使用该 page_content 构建页面上下文:
- 初始化页面 state;
- 解析方法
onClickNew1,并注入上下文; - 注入页面级 CSS Scope。
5).调用 renderer 按照 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 的核心链路可以概括为:
perl
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 负责:
- 基于
pageId选择当前页面的pageSchema; - 构建页面上下文(state、route、router、stores、dataSourceMap、utils、cssScopeId 等);
- 解析页面定义的 methods 和 state;
- 调用 renderer 渲染页面。
- 基于
- PageRenderer 是对外的高阶组件,外部只需传入
-
renderer(render.ts):
- 核心渲染器,把 schema 节点映射为真实组件 VNode。
- 处理组件解析、属性解析、循环、条件、插槽、区块与 CSS 作用域等。
-
parser(parser.ts):
- 配置解析引擎,把 JSExpression / JSFunction / i18n / 插槽等配置形式统一解析成运行时值或函数。
-
page-function 系列:
- 提供页面级 state 管理、CSS Scope 管理、Block 上下文等能力。
3.2 三层上下文
为了让表达式和函数在运行时拥有完整信息,runtime-renderer 构建了三层上下文:
- 应用级上下文 :物料组件、数据源集合
dataSourceMap、国际化配置、工具函数(utils)、应用级 CSS、router、stores 等。 - 页面级上下文:页面 state、当前路由信息、page 级 CSS Scope Id、页面 methods 和生命周期配置。
- 区块级上下文 :区块自己的 state 和 CSS Scope,通过
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 约定拉取基础物料包,再根据 Schema 中的 componentsMap 精细加载具体组件 。加载完成后,组件会被挂到全局对象(如 window.TinyLowcodeComponent / window.TinyComponentLibs),以便渲染阶段通过组件名查找对应实现。
4.1.3 初始化 importMap 与第三方依赖
对于在/mock/bundle.json中引入的包需要的子依赖和其他通过 CDN 引入的第三方库,runtime-renderer 使用 importMap 做统一映射:
- 在
import-map.json中维护包名到实际 CDN 地址的映射; - 启动时将 importMap 注入到浏览器环境,使动态加载的模块可以直接用包名引用。
4.1.4 初始化国际化配置
应用级 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 包作为工具函数使用。
2). 函数型工具函数(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 初始化全局变量
runtime-renderer 基于 Pinia 来管理运行时的全局变量,即 stores:
- 启动入口
initRuntimeRenderer中,会先调用generateStoresConfig(),根据 App Schema 中的全局状态配置等生成一份标准的 stores 配置; - 然后创建 Pinia 实例,并通过
createStores(storesConfig, pinia)将这些配置注册为实际的 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会把它挂到应用上,使页面可以通过 hash 路由进行切换。
4.2 页面级渲染入口
页面级渲染的核心是两个组件:对外暴露的 PageRenderer,以及真正做事的 RenderMain。
4.2.1 PageRenderer:对外形态
对使用方来说,只需要:
vue
<PageRenderer :pageId="currentPageId" />
PageRenderer 内部会把 pageId 透传给 RenderMain,对外隐藏所有与 Schema 解析和上下文构建相关的细节。
4.2.2 从 pageId 到 pageSchema
RenderMain 在 setup 中会:
- 通过
useAppSchema().getPageById(pageId)找到对应页面对象; - 从中取出
page_content作为当前页面的 schema; - 用
computed包装,确保后续 Schema 更新可以被捕捉; - 对
page_content做一次深拷贝,避免渲染过程中意外修改原始数据。
随后使用 watch 监听当前 schema:
- 首次进入页面时立即执行一次,调用
setSchema完成初始化; - 后续如果设计器更新了该页面并同步到运行时,再次触发
setSchema,实现设计态 → 运行态的实时联动。
4.2.3 页面上下文的构建
setSchema 是 RenderMain 的关键逻辑,它会基于当前 pageSchema 构建出页面级上下文:
- 从路由系统获取
route、router; - 通过依赖注入拿到全局
stores; - 通过 app-function 获取
dataSourceMap和utils; - 使用
useState初始化页面级state与setState; - 生成当前页面的
cssScopeId,例如data-te-page-<pageId>。
这些信息被组合成 contextData,在 setSchema 开头通过 setContext(contextData, true) 注入运行时上下文:
true表示清空旧上下文,避免页面切换或 Schema 更新时残留状态。- 后续解析 methods 和 state 时,都会在这个上下文中执行。
4.2.4 方法与状态的初始化顺序
在 setSchema 内部,初始化顺序大致为:
1). 设置上下文环境 :先调用 setContext(contextData, true),确保 this.state、this.stores、this.dataSourceMap、this.utils 等在之后解析中都可用。 2). 解析并注入 methods :对 schema 中的 methods 逐项执行 parseData:
- 将 JSFunction 字符串解析为真实函数;
- 使用
generateFn包装,让其在执行时带上完整上下文并具备异常兜底; - 放入
methods容器,并合入 context。
3). 初始化 state :调用 setState(newSchema.state, true):
- 根据 defaultValue 填充 state;
- 对带 accessor 的字段记录 getter / setter 行为;
- 在很多场景下,state 中的表达式会依赖 props、utils、stores、methods,因此需要放在 methods 之后。
4). 注入页面级 CSS :调用 setPageCss(pageSchema.css, cssScopeId):
- 为当前页面注入带
[data-te-page-<id>]前缀的样式; - renderer 渲染节点时会自动附加该 attribute,实现样式隔离。
这样的顺序可以保证上下文完整,避免出现"方法或状态在解析时访问不到依赖"的情况。
4.2.5 Render 函数中的根容器
RenderMain 的 render 函数不会直接把 pageSchema.children 交给 renderer,而是先构造一个根容器:
ts
const rootChildrenSchema = {
componentName: 'div',
props: { ...(pageSchema.props || {}) },
children: pageSchema.children
}
- 这样能与"出码"的根结构保持一致,也便于统一挂载页面级样式和属性。
- 若
pageSchema.children非空,则渲染:
ts
h(renderer, { schema: rootChildrenSchema, parent: pageSchema })
- 若 children 为空,则渲染一个
Loading组件,避免页面完全空白。
4.3 核心渲染器:从 Schema 到 VNode
renderer 负责把 Schema 节点转成 Vue VNode,parser 负责把各种配置数据解析成运行时值,两者协同完成渲染。
4.3.1 组件解析
根据节点的 componentName,renderer 会按以下顺序查找对应实现:
1). 内置 Canvas 系列组件映射(如 Text、Img、RouterLink、Collection 等)。 2). 运行时加载的 TinyVue 组件和 window.TinyLowcodeComponent 中注册的物料组件。 3). 自定义元素(Web Components),通过 customElements 映射表预留扩展点。 4). 原生 HTML 标签:如果 componentName 是合法 HTML 标签,直接作为标签名使用。 5). 区块组件:如果在 window.blocks 中找到同名 block,则:
- 动态创建一个 Vue 组件;
- 在组件内部基于 block 的 schema 和 block 上下文递归渲染 children;
- 使用 block 独立的 CSS Scope Id。
若以上都未命中,则使用占位组件(如 CanvasPlaceholder)兜底,保证渲染不因单个节点错误而中断。
4.3.2 属性解析与 CSS Scope
Schema 中的 props 可能包含多种形式:普通值、JSExpression、JSFunction、状态访问器、图标配置、插槽声明等。renderer 会通过 parseData 对其统一解析,生成"干净"的 props 对象:
- JSExpression:在当前 scope + 上下文下执行表达式,得到最终值;
- JSFunction:解析为真实函数并绑定上下文;
- 状态访问器:按默认值或 getter 逻辑解析;
- 插槽声明:根据配置生成对应的 Slot 函数;
- 其他对象和数组属性:递归调用
parseData。
在此基础上,renderer 会:
- 根据 scope 或 context 中的
cssScopeId,给非 Block 组件自动添加形如[data-te-page-xxx]: ''的属性,用于样式作用域隔离; - 对 Canvas 和 Block 组件额外挂上
schema字段,便于组件内部根据 Schema 进行渲染; - 将
className重命名为class,避免覆盖组件内部样式约定。
4.3.3 循环、条件与作用域
循环和条件渲染通过 loop、loopArgs 和 condition 三个字段来描述:
loop:通常是 JSExpression,返回一个数组;loopArgs:描述 item 和 index 在表达式中的名称,例如['row', 'i'];condition:JSExpression,决定是否渲染该节点。
renderer 的流程是: 1). 使用 parseData(loop, scope, context) 得到循环数组。 2). 对每一个 item,调用 parseLoopArgs 生成局部作用域(如 { row, i })。 3). 合并到当前作用域,得到 mergeScope。 4). 用 parseCondition(condition, mergeScope, context) 判断是否渲染该节点。 5). 在 mergeScope 下解析 children 和 props,生成对应 VNode。
如果没有配置 loop,则在当前 scope 下渲染一次节点即可。
4.3.4 children 与插槽
children 的处理有多种情况:
- 若组件被标记为容器且 children 为空,会自动注入
CanvasPlaceholder,提升设计和调试体验。 - 若 children 不是数组且本身是表达式,则直接调用
parseData(children, scope, context),常用于 Text / 简单插值场景。 - 若 children 是普通数组且不包含 Template,则通过
renderGroup递归渲染每个子节点。 - 若 children 中包含
componentName: 'Template':- 使用
generateSlotGroup按 slotName 分组; - 为每个 slot 生成形如
($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,并在多层上下文中安全执行表达式与函数。
对于设计器使用者来说,它提供了一条"所见即所得"的运行路径。
关于OpenTiny
欢迎加入 OpenTiny 开源社区。添加微信小助手:opentiny-official 一起参与交流前端技术~
OpenTiny 官网:opentiny.design
OpenTiny 代码仓库:github.com/opentiny
TinyVue 源码:github.com/opentiny/ti...
TinyEngine 源码: github.com/opentiny/ti...
欢迎进入代码仓库 Star🌟TinyEngine、TinyVue、TinyNG、TinyCLI、TinyEditor~ 如果你也想要共建,可以进入代码仓库,找到 good first issue 标签,一起参与开源贡献~