核心概念

数据流

从定义到渲染的完整链路——开发态与生产态的数据流向

本页详细讲解 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

ktr/template/hello/card/data/default.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.html

4. 捕获数据实时查看

# 监听 captured.json 变化
watch -n 1 cat ktr/template/hello/card/data/captured.json

下一步

On this page