本文由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 , 如果需要設計器內內容有更新的話則需要重新加載運行時頁面以同步。
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……
(本項目為開源之夏活動貢獻,歡迎大家體驗並使用)源碼可參考:https://github.com/opentiny/tiny-engine/tree/ospp-2025/runtime-rendering
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 負責:
- 基於
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:對外形態
對使用方來説,只需要:
<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,而是先構造一個根容器:
const rootChildrenSchema = {
componentName: 'div',
props: { ...(pageSchema.props || {}) },
children: pageSchema.children
}
- 這樣能與“出碼”的根結構保持一致,也便於統一掛載頁面級樣式和屬性。
- 若
pageSchema.children非空,則渲染:
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,並在多層上下文中安全執行表達式與函數。
對於設計器使用者來説,它提供了一條“所見即所得”的運行路徑。
(本項目為開源之夏活動貢獻,歡迎大家體驗並使用)源碼可參考:https://github.com/opentiny/tiny-engine/tree/ospp-2025/runtime-rendering
關於OpenTiny
歡迎加入 OpenTiny 開源社區。添加微信小助手:opentiny-official 一起參與交流前端技術~
OpenTiny 官網:https://opentiny.design
OpenTiny 代碼倉庫:https://github.com/opentiny
TinyVue 源碼:https://github.com/opentiny/tiny-vue
TinyEngine 源碼: https://github.com/opentiny/tiny-engine
歡迎進入代碼倉庫 Star🌟TinyEngine、TinyVue、TinyNG、TinyCLI、TinyEditor~
如果你也想要共建,可以進入代碼倉庫,找到 good first issue 標籤,一起參與開源貢獻~