核心概念
数据流
从定义到渲染的完整链路——开发态与生产态的数据流向
本页详细讲解 ktr 框架中数据的完整流转路径,覆盖开发态和生产态两种场景。
完整数据链路概览
模板定义(defineTemplate)
↓
注册表生成(.ktr/)
↓
┌─────────┴─────────┐
↓ ↓
开发态 生产态
面板→沙盒→渲染 SSR→HTML→截图开发态数据流
开发态的数据流经过面板 → 沙盒 → 渲染三个环节。
1. 用户操作触发
用户点击模板 'hello/card'
↓
用户选择数据源 'basic'
↓
面板发起 API 请求2. Mock API 响应
面板通过 RESTful API 获取数据:
// 面板前端代码(React)
async function (: string, : string) {
// GET /__ktr/api/data?path=hello/card&name=basic
const = await (`/__ktr/api/data?path=${}&name=${}`)
return await .()
}服务端 Mock API 处理逻辑:
export async function (: any, : any) {
// GET /__ktr/api/data?path=hello/card&name=default
const = new (.url, 'http://localhost')
const = ..('path')!
const = ..('name')!
// 优先级 1:JSON mock(面板可编辑)
const = .(., , 'data', `${}.json`)
if (.()) {
const = .(.(, 'utf-8'))
return (, 200, { , : 'json', : false, })
}
// 优先级 2:TS mock(面板只读)
const = (await ()).(() => . === )
if () {
return (, 200, { , : 'ts', : true, : . })
}
// 未找到
(, 404, { : 'Data entry not found' })
}数据源优先级:JSON mock > TS mock(同名时 JSON 优先,TS mock 在面板中只读)
3. 面板下发数据
面板通过 postMessage 将数据发送到沙盒 iframe:
// 面板代码
function (: string, : any, ?: any) {
const = .('#sandbox-iframe') as HTMLIFrameElement
.?.(
{
: 'ktr-panel',
: 'ktr:data',
: { , , }
},
'*'
)
}消息协议:
// 面板 → 沙盒消息类型
type =
| { : 'ktr-panel'; : 'ktr:select'; : { : string } }
| { : 'ktr-panel'; : 'ktr:data'; : { : string; : unknown; ?: <string, unknown> } }
| { : 'ktr-panel'; : 'ktr:theme'; : { ?: <ThemeContext> } }
| { : 'ktr-panel'; : 'ktr:inspect'; : { : boolean } }
// 沙盒 → 面板消息类型
type =
| { : 'ktr-sandbox'; : 'ktr:ready'; : { : <{ : string; ?: string }> } }
| { : 'ktr-sandbox'; : 'ktr:register-progress'; : { : number; : number; : string } }
| { : 'ktr-sandbox'; : 'ktr:rendered'; : { : string; : number; ?: { : number; : number } } }
| { : 'ktr-sandbox'; : 'ktr:error'; : { ?: string; : string } }4. 沙盒渲染组件
沙盒 iframe 加载虚拟模块 virtual:ktr-sandbox,监听面板消息并渲染:
// 1. 预加载所有模板组件
const : <string, any> = {}
// 逐个动态导入(串行,可见进度)
const = ['hello/card', 'user/profile']
for (let = 0; < .; ++) {
const = []
[] = (await ()).
// 上报加载进度
..(
{
: 'ktr-sandbox',
: 'ktr:register-progress',
: { : + 1, : ., }
},
'*'
)
}
// 通知面板加载完成
..(
{
: 'ktr-sandbox',
: 'ktr:ready',
: { : .(() => ({ })) }
},
'*'
)
// 2. 监听面板消息
let : Root | null = null
.('message', () => {
if (..type === 'ktr:data') {
const { , , } = ..payload
(, , )
}
})
// 3. 渲染函数
async function (: string, : any, ?: any) {
const = []
if (!) {
.(`Template not found: ${}`)
return
}
const = .('container')
if (!) return
// 创建或复用 React root
if (!) {
= ()
}
// 渲染组件
.(< ={} ={} />)
// 4. 等待渲染稳定,测量尺寸
await ()
const { , } = .()
// 5. 上报渲染完成
..(
{
: 'ktr-sandbox',
: 'ktr:rendered',
: { , : 0, : { , } }
},
'*'
)
}
// 等待元素尺寸稳定(连续 3 帧无变化)
async function (: HTMLElement) {
let = 0
let = { : 0, : 0 }
while ( < 3) {
await new (() => ())
const { , } = .()
if ( === . && === .) {
++
} else {
= 0
}
= { , }
}
}5. 面板更新画布
面板接收沙盒上报的尺寸,调整 iframe 显示:
// 面板代码
.('message', () => {
if (..type === 'ktr:rendered') {
const { , } = ..payload
// 更新画布尺寸显示
(.width, .height)
// 自动适应视口
()
}
})
function (: number, : number) {
const = .('#sandbox-iframe') as HTMLIFrameElement
.. = `${}px`
.. = `${}px`
}
function () {
const = .('.canvas-container') as HTMLElement
const = .('iframe') as HTMLIFrameElement
const = .()
const = .()
// 计算缩放比例(留 20px 边距)
const = (. - 40) / .
const = (. - 40) / .
const = .(, , 1)
.. = `scale(${})`
}开发态数据流图
┌─────────────────────┐
│ 用户操作 │ 选择模板 'hello/card' + 数据 'basic'
└──────────┬──────────┘
↓
┌─────────────────────┐
│ 开发面板 │ GET /__ktr/api/data?path=hello/card&name=basic
│ (React) │
└──────────┬──────────┘
↓
┌─────────────────────┐
│ Mock API │ 1. 优先读取 data/basic.json
│ (Vite 中间件) │ 2. 回退到 mock.ts 的 basic 导出
│ │ 3. 返回 JSON 数据
└──────────┬──────────┘
↓
┌─────────────────────┐
│ 面板前端 │ postMessage({ type: 'ktr:data', payload: { path, data, ctx } })
└──────────┬──────────┘
↓
┌─────────────────────┐
│ 沙盒 iframe │ 1. 接收消息
│ (React 环境) │ 2. 动态 import 组件
│ │ 3. root.render(<Component data={data} ctx={ctx} />)
│ │ 4. 测量尺寸
└──────────┬──────────┘
↓
┌─────────────────────┐
│ 沙盒回传 │ postMessage({ type: 'ktr:rendered', size: { width, height } })
└──────────┬──────────┘
↓
┌─────────────────────┐
│ 面板画布 │ 1. 更新 iframe 尺寸
│ │ 2. 缩放适应视口
│ │ 3. 显示完成状态
└─────────────────────┘生产态数据流
生产态的数据流经过 SSR → HTML → 截图三个环节。
1. Karin 插件调用
// 用户插件代码
import { } from './utils/render'
// Karin 指令处理函数
export async function (: any) {
const = await (
'hello/card',
{
: 'Karin Template React',
: [
{ : '渲染方式', : 'SSR HTML' },
{ : '框架', : 'React 19' }
]
},
{
: { : 'dark', : 'oklch(0.62 0.19 254)' }
}
)
await .reply()
}2. 渲染器初始化
renderImage 内部调用 createTemplateRenderer:
export function (: string) {
// 1. 定位包根(从 import.meta.url 向上找 package.json)
const = ()
// 惰性初始化:首次渲染时才解析配置、加载注册表
let : <<typeof >> | undefined
return async (: string, : any, ?: any) => {
??= (async () => {
// 2. 加载配置(karin.template.ts + 默认值;渲染器自身在 bundle 里时跳过,直接用默认值)
const = await ({ : })
// 3. 加载注册表(bundle 里直接用产物;否则优先 .ktr/ 源文件,回退到打包产物)
const = await ({ })
// 4. 创建渲染器(CSS 开发态用缓存、生产态用打包产物)
return (, {
: (),
: `${.}/html`,
: .
})
})()
const = await
return (, , )
}
}3. SSR 渲染生成 HTML
export function (: <string, any>, : any) {
const { , = [] } =
return async function (: string, : any, ?: any) {
// 1. 获取模板定义
const = []
if (!) {
return { : false, : '', : `Template is not registered: ${}` }
}
// 2. 执行 beforeRender 钩子
await (, { : , , })
// 3. React 流式 SSR:等 allReady 后一次性读出完整 HTML
const = await (React.(.component, { , }))
await .
const = await new ().()
// 4. 执行 afterRender 钩子(可加工 HTML)
const = await (, , { : , , })
// 5. 包装 HTML 外壳(内联 CSS + 主题变量),写入文件
const = .(/\//g, '_') + '.html'
const = .(, )
.(, { : true })
.(, (, ), 'utf-8')
return { : true, }
}
}4. HTML 包装器
export function (: string, : { ?: <ThemeContext> }): string {
const { } =
// 只注入显式提供的主题字段;未提供时不输出任何变量,组件库自身主题生效
const = ? () : ''
const = ?.
return `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<style>${}</style>
</head>
<body class="${ === 'dark' ? 'dark' : ''}"${ ? ` data-theme="${}"` : ''} style="${}">
<div id="container">${}</div>
</body>
</html>`
}
function (: <ThemeContext>): string {
const : string[] = []
// 变量名与 HeroUI 语义色一一对应,只输出显式提供的字段
if (.) .(`--accent: ${.}`)
if (.) .(`--background: ${.}`)
if (.) .(`--foreground: ${.}`)
if (.) .(`--surface: ${.}`)
if (.) .(`--border: ${.}`)
// 其余语义色同理,theme.vars 里的任意变量追加在最后
return .('; ')
}5. Puppeteer 截图
Karin 框架接收 HTML 路径,使用 Puppeteer 截图:
// Karin 框架内部代码(示意)
import from 'puppeteer'
export async function (: string): <> {
const = await .({
: true,
: ['--no-sandbox', '--disable-setuid-sandbox']
})
const = await .()
// 加载 HTML 文件
await .(`file://${}`, {
: 'networkidle0'
})
// 获取 #container 元素尺寸
const = await .('#container')
if (!) {
throw new ('Container element not found')
}
// 截图
const = await .({
: 'png',
: true // 透明背景
})
await .()
return
}生产态数据流图
┌─────────────────────────┐
│ Karin 插件调用 │ renderImage('hello/card', data, ctx)
└──────────┬──────────────┘
↓
┌─────────────────────────┐
│ createTemplateRenderer │ 1. 定位包根
│ │ 2. 加载配置
│ │ 3. 加载注册表(.ktr/ 或打包产物)
│ │ 4. 解析 CSS 路径
└──────────┬──────────────┘
↓
┌─────────────────────────┐
│ createRenderer │ 1. 获取模板组件
│ │ 2. 执行 beforeRender 钩子
└──────────┬──────────────┘
↓
┌─────────────────────────┐
│ React SSR │ renderToReadableStream(<Component data={data} ctx={ctx} />)
│ │ → 生成 HTML 片段
└──────────┬──────────────┘
↓
┌─────────────────────────┐
│ 插件钩子 │ afterRender() → 可修改模板 HTML 片段
└──────────┬──────────────┘
↓
┌─────────────────────────┐
│ HTML 包装器 │ 1. 内联 CSS
│ │ 2. 注入主题变量到 <body> style
│ │ 3. 添加 <html>、<head>、<meta>
└──────────┬──────────────┘
↓
┌─────────────────────────┐
│ 写入文件 │ dist/template/html/hello_card.html
└──────────┬──────────────┘
↓
┌─────────────────────────┐
│ Puppeteer 截图 │ 1. 加载 HTML
│ │ 2. 查找 #container
│ │ 3. 截取元素截图
│ │ 4. 返回 PNG Buffer
└─────────────────────────┘Mock 数据的三种来源
1. TS mock(mock.ts)
// ktr/template/hello/card/mock.ts
import type { } from './index'
export const = {
: 'Karin Template React',
: [{ : '渲染方式', : 'SSR HTML' }]
} satisfies
export const = {
: '空状态',
: []
} satisfies 特点:
- ✅ 类型安全:
satisfies编译期校验 - ✅ 代码复用:插件代码可直接 import
- ❌ 面板只读:不可在面板编辑
- 📍 适合场景:固定示例数据,作为标准参考
2. JSON mock(data/*.json)
{
"title": "Karin Template React",
"items": [
{ "label": "渲染方式", "value": "SSR HTML" },
{ "label": "样式方案", "value": "Tailwind CSS v4" }
]
}特点:
- ❌ 无类型校验:运行时解析
- ✅ 面板可编辑:新建/编辑/删除/保存
- ✅ 快速调试:边改数据边看效果
- 📍 适合场景:多组对照数据,快速迭代样式
3. 捕获数据(data/captured.json)
自动生成,无需手写:
// runtime/capture.ts(简化示意)
import * as from 'node:fs'
import * as from 'node:path'
export function (: string, : any, : any, : string) {
const = .(, , 'data')
// 目录不存在时自动创建
.(, { : true })
// 写入 { data, ctx } 完整快照(覆盖,只保留最近一次)
.(.(, 'captured.json'), .({ , }, null, 2))
}特点:
- 🤖 自动记录:每次真实渲染自动写入
- 📡 实时推送:SSE 推送给面板,自动刷新
- 🐛 问题复现:拿线上真实数据调试
- 📍 适合场景:排查渲染问题,验证边缘情况
数据源优先级
Mock API 的加载优先级:JSON mock > TS mock
async function (: string, : string) {
// 1. 优先 JSON mock(面板可编辑)
const = `${}/data/${}.json`
if (.()) {
return .(.(, 'utf-8'))
}
// 2. 回退 TS mock(面板只读)
const = await ()
if ([]) {
return []
}
throw new ('Data entry not found')
}同名时 JSON mock 优先;TS mock 在面板中显示只读标记。
主题变量下发机制
开发态:面板调整主题
用户调整主题抽屉
↓
面板更新本地状态
↓
postMessage({ type: 'ktr:theme', payload: { theme } })
↓
沙盒接收并重新渲染
↓
组件通过 ctx.theme 访问主题function (: any) {
// 发送给沙盒
.?.(
{
: 'ktr-panel',
: 'ktr:theme',
: { }
},
'*'
)
// 同时更新面板本地状态
()
}.('message', () => {
if (..type === 'ktr:theme') {
const { } = ..payload
// 重新渲染当前模板,传入新主题
(, , { ..., })
}
})生产态:渲染选项传入
await ('hello/card', , {
: {
: 'dark',
: 'oklch(0.62 0.19 254)',
: 'oklch(0.15 0.01 254)',
: 'oklch(0.95 0.01 254)'
}
})框架将主题变量写入 HTML 的 <body> 样式:
<body
class="dark"
data-theme="dark"
style="--accent: oklch(0.62 0.19 254); --background: oklch(0.15 0.01 254); --foreground: oklch(0.95 0.01 254)"
>
<div id="container">
<!-- 组件内容 -->
</div>
</body>所有后代元素继承 CSS 变量,HeroUI 组件自动应用主题。
框架不发明默认主题
重要:框架不发明默认主题色。ctx.theme 只包含调用方显式提供的字段;没人显式设置时,SSR
和面板沙盒都不注入任何颜色变量,组件库按自身默认主题渲染。
// runtime/html-wrapper.ts(示意)
function (?: any): string {
if (!) return '' // 未提供主题,不注入任何变量
const : string[] = []
// 只输出显式提供的字段
if (.accent !== ) .(`--accent: ${.accent}`)
if (.background !== ) .(`--background: ${.background}`)
// ... 其他字段
return .('; ')
}数据流调试技巧
1. 查看 Mock API 响应
# 开发服务器启动后
curl "http://localhost:5180/__ktr/api/data?path=hello/card&name=basic"2. 监听 postMessage 通信
// 浏览器控制台
window.addEventListener('message', (event) => {
console.log('[postMessage]', event.data)
})3. 检查生成的 HTML
# 生产渲染后
cat dist/template/html/hello_card.html4. 捕获数据实时查看
# 监听 captured.json 变化
watch -n 1 cat ktr/template/hello/card/data/captured.json