核心概念

开发面板架构

面板 + 沙盒的分离设计、postMessage 通信协议和技术实现细节

ktr 开发面板采用面板 + 沙盒分离架构:面板是预构建的 React 静态应用,用户模板在 iframe 沙盒里渲染,两者通过 postMessage 通信,确保调试界面与模板渲染环境互不干扰。

架构设计

整体架构图

┌──────────────────────────────────────────────────────┐
│             Vite Dev Server (localhost:5180)         │
│                                                      │
│  ┌────────────────────┐  ┌─────────────────────┐     │
│  │   面板中间件        │  │  沙盒虚拟模块        │     │
│  │  /__ktr/panel      │  │  virtual:ktr-sandbox │     │
│  │  (预构建静态应用)   │  │  (动态生成)          │     │
│  └────────────────────┘  └─────────────────────┘     │
│                                                      │
│  ┌──────────────────────────────────────────────┐    │
│  │   Mock API (/__ktr/api)                      │    │
│  │   - GET /templates        (模板列表)         │    │
│  │   - GET /data             (列表/读取数据)    │    │
│  │   - PUT /data             (保存 JSON)        │    │
│  │   - DELETE /data          (删除 JSON)        │    │
│  │   - GET /stream           (SSE 数据推送)     │    │
│  └──────────────────────────────────────────────┘    │
│                                                      │
│  ┌──────────────────────────────────────────────┐    │
│  │   WebSocket HMR                              │    │
│  │   - 模板源码热更新(沙盒内生效)             │    │
│  └──────────────────────────────────────────────┘    │
└──────────────────────────────────────────────────────┘

┌──────────────────────┐          ┌──────────────────────┐
│   开发面板            │          │  沙盒 iframe          │
│   (React)            │◄────────►│  (React 环境)         │
│                      │postMessage│                      │
│  - 模板导航           │          │  - 动态 import 组件   │
│  - 数据选择/编辑      │          │  - React 渲染        │
│  - 主题构建器         │          │  - 尺寸测量上报      │
│  - 画布缩放/平移      │          │  - 错误边界          │
└──────────────────────┘          └──────────────────────┘

分离设计的优势

方面传统方案ktr 分离架构
样式隔离面板样式污染模板iframe 完全隔离
React 上下文共享 context,易冲突独立 root,互不干扰
错误隔离模板错误影响面板沙盒崩溃不影响面板
热更新整体刷新模板更新不刷新面板

面板前端

技术栈

  • 框架:React 19 + HeroUI(@heroui/react
  • 样式:Tailwind CSS v4
  • 路由:react-router-dom,模板地址 /templates/<板块>/<模板>?data=<数据名>,可分享、可刷新
  • 画布:自研 GSAP 变换引擎(panel/canvas/,滚轮光标锚点缩放、拖拽惯性平移、双击/快捷键适应)
  • 分发形式:随包预构建为静态资源(panel/dist),dev server 的面板中间件挂在 /__ktr/panel 托管,不需要下游构建面板源码

核心功能模块

面板源码在 packages/core/panel,主要组件:

模块组件职责
模板导航App.tsx按板块分组展示模板树,路由同步当前选中项
数据选择DataFileSelector.tsx列出 TS mock / JSON mock / captured.json,切换数据源
数据编辑JsonEditor.tsxMockDataEditorModal.tsx编辑并保存 JSON mock(TS mock 只读)
主题ThemeBuilderPanel.tsxPanelThemeSelect.tsx构建下发给模板的主题变量;切换面板自身明暗
预览画布PreviewPanel.tsx承载沙盒 iframe,缩放/平移/截图预览

加载模板列表

面板从 Mock API 读取模板清单(每次读取前 dev server 会先刷新 .ktr 注册表,新增模板无需重启):

interface TemplateMeta {
  : string
  ?: string
  ?: string
}

export function () {
  const [, ] = <TemplateMeta[]>([])

  (() => {
    // GET /__ktr/api/templates → { templates: [...] }
    ('/__ktr/api/templates')
      .(() => .())
      .(() => (.templates))
  }, [])

  return 
}

下发数据到沙盒

用户选择模板和数据后,面板把路由、数据和运行时上下文一起发给沙盒:

function (: string, : unknown) {
  const  = ()
  .?.(
    {
      : 'ktr-panel',
      : 'ktr:data',
      : { , , : () }
    },
    ..
  )
}

沙盒虚拟模块

生成逻辑

沙盒入口是 Vite 虚拟模块 virtual:ktr-sandbox,由 sandboxPluginload 阶段动态生成:约定扫描出全部模板路由后,逐路由生成 /@fs/ 动态导入,并引入 CSS 入口:

export async function (): <string> {
  // 逐路由动态导入而不是整包导入 .ktr 注册表:
  // 注册表是单模块,导入即同步拉起全部模板,无法按模块粒度上报进度
  const  = await (.)
  const  = 
    .(() => `  ['${.}', () => import('/@fs/${.(., .)}')]`)
    .(',\n')

  return `
import React from 'react'
import { createRoot } from 'react-dom/client'

const templateLoaders = [
${}
]
const templates = {}

// 逐个加载模板,每完成一个上报一次进度
for (const [route, load] of templateLoaders) {
  templates[route] = (await load()).default
  window.parent.postMessage({
    source: 'ktr-sandbox',
    type: 'ktr:register-progress',
    payload: { loaded: Object.keys(templates).length, total: templateLoaders.length, path: route }
  }, window.location.origin)
}

// 全部加载完,上报模板元信息
window.parent.postMessage({
  source: 'ktr-sandbox',
  type: 'ktr:ready',
  payload: { templates: Object.entries(templates).map(([path, def]) => ({ path, name: def.name, description: def.description })) }
}, window.location.origin)
`
}

渲染与主题

沙盒内维护当前选中的路由、数据和上下文,收到 ktr:data / ktr:theme 消息后重新渲染。主题变量同时写到 htmlbody 上,只注入面板显式提供的字段:

const  = [
  ['--background', 'background'],
  ['--foreground', 'foreground'],
  ['--accent', 'accent'],
  ['--accent-foreground', 'accentForeground']
  // 其余语义色同理
] as 

function (: <ThemeContext>) {
  const  = . === 'dark' ? 'dark' : 'light'
  ...('dark',  === 'dark')
  ... = 

  for (const [, ] of ) {
    const  = []
    // 显式提供的写入,未提供的移除——框架不发明默认色,组件库自身主题生效
    if (typeof  === 'string' && ) {
      ...(, )
    } else {
      ...()
    }
  }
}

渲染出错时由错误边界接管:用固定尺寸的占位卡替换模板位置,画布的测量、缩放、拖拽行为与正常模板一致,同时向面板上报 ktr:error

尺寸测量与上报

React 渲染是异步的,沙盒按帧轮询容器尺寸,连续几帧稳定后才上报 ktr:rendered,避免把半截高度发给面板:

function (: number) {
  let  = 0
  let  = 0
  let  = 0

  const  = () => {
    const  = .('container')!.()
    const  = .(1, .(.))
    const  = .(1, .(.))

     =  ===  &&  ===  ?  + 1 : 0
     = 
     = 

    if ( < 3) {
      ()
      return
    }

    ('ktr:rendered', {
      : ,
      : .(.() - ),
      : { ,  }
    })
  }

  ()
}

postMessage 通信协议

所有消息都带 source 字段('ktr-panel' / 'ktr-sandbox')区分方向,目标 origin 限定为当前窗口源。

消息类型定义

// ===== 面板 → 沙盒消息 =====
export type  =
  | { : 'ktr-panel'; : 'ktr:select'; : { : string } }
  | { : 'ktr-panel'; : 'ktr:data'; : { : string; : unknown; ?: <string, unknown> } }
  | { : 'ktr-panel'; : 'ktr:theme'; : { ?: <ThemeContext> } }
  | { : 'ktr-panel'; : 'ktr:panel-theme'; : { : boolean } }
  | { : 'ktr-panel'; : 'ktr:inspect'; : { : boolean } }

// ===== 沙盒 → 面板消息 =====
export type  =
  | { : 'ktr-sandbox'; : 'ktr:ready'; : { : <{ : string; ?: string; ?: string }> } }
  | { : 'ktr-sandbox'; : 'ktr:register-progress'; : { : number; : number; : string } }
  | { : 'ktr-sandbox'; : 'ktr:rendered'; : { : string; : number; ?: { : number; : number } } }
  | { : 'ktr-sandbox'; : 'ktr:error'; : { ?: string; : string; ?: string } }
  | { : 'ktr-sandbox'; : 'ktr:hmr'; : { : string } }

ktr:panel-theme 控制的是 iframe 画布背板的明暗(来自面板外壳主题),与下发给模板的 ktr:theme 解耦。

通信流程

1. 初始化

面板加载

创建 iframe(src="/__ktr/sandbox")

iframe 加载 virtual:ktr-sandbox

沙盒逐个 import 模板组件

每加载一个,发送 ktr:register-progress

全部加载完,发送 ktr:ready(携带模板元信息)

面板收到 ready,展示模板树

2. 渲染模板

用户点击模板

面板发送 ktr:select { path }

面板从 Mock API 加载数据列表,用户选择数据

面板发送 ktr:data { path, data, ctx }

沙盒渲染组件(错误边界兜底)

沙盒等待尺寸稳定(连续 3 帧无变化)

沙盒发送 ktr:rendered { path, elapsed, size }

面板按上报尺寸调整画布

3. 主题切换

用户在主题构建器中调整

面板发送 ktr:theme { theme }

沙盒把显式提供的变量写到 html/body,移除未提供的

沙盒重新渲染当前模板

沙盒发送 ktr:rendered { size }

Mock API 设计

Mock API 是挂在 Vite dev server 上的中间件(/__ktr/api 前缀),不是独立的 Express 服务。

接口一览

方法路径说明
GET/__ktr/api/templates模板列表(读取前先刷新 .ktr 注册表)
GET/__ktr/api/data?path=<路由>数据源列表 { entries: [{ name, source, readonly }] }
GET/__ktr/api/data?path=<路由>&name=<名称>读取单条数据
PUT/__ktr/api/data?path=<路由>&name=<名称>保存 JSON mock
DELETE/__ktr/api/data?path=<路由>&name=<名称>删除 JSON mock
GET/__ktr/api/stream数据文件变更的 SSE 推送

读取单条数据时同名 JSON 优先于 TS mock;TS mock 在面板中只读(PUT 返回 409),因为它是类型安全的源码,应直接编辑文件。captured.json{ data, ctx } 完整快照,接口会解包后连同 ctx 一起下发,面板回放时能还原后端真实下发的主题等上下文。

export async function (: any, : any) {
  const  = new (.url, 'http://ktr.local')
  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' })
}

SSE 数据推送

dev server 监听 mock 目录下的 data/*.json 变更(复用 Vite 的 watcher,按模板路由 100ms 去抖),通过 /__ktr/api/stream 的 SSE 通道推送给面板。事件名固定为 ktr:data-files-changed,负载为 { templatePath, file }

export function (: ViteDevServer) {
  ..(.)

  const  = (: string) => {
    // 只有各模板 data/ 子目录里的 JSON 才推送给面板
    if (!.('.json') || !.('data')) return

    // 去抖后广播:{ templatePath: 'hello/card', file: 'captured.json' }
    ({ : 'hello/card', : .('/').()! })
  }

  ..('add', )
  ..('change', )
  ..('unlink', )
}

面板侧用 EventSource 监听,captured.json 变更时自动刷新并选中——真实渲染一发生,面板里就能立刻看到这份数据:

export function () {
  const  = new ('/__ktr/api/stream')

  .('ktr:data-files-changed', () => {
    const { ,  } = .(( as ).)
    (, )
  })

  return () => .()
}

用 SSE 而不是 Vite 的 HMR WebSocket:Vite 8 的 HMR WebSocket 对浏览器同源连接强制校验 token,面板内手写 WS 客户端拿不到 token,SSE 通道不受此限制。

WebSocket HMR

模板源码的热更新走 Vite 自带的 HMR:模板文件保存后,沙盒 iframe 里的模块直接热替换并重新渲染(沙盒随后向面板上报 ktr:hmr)。面板自身不受影响,因为与沙盒隔离。

下一步

On this page