核心概念
模板注册表
.ktr/ 三个文件的作用、生成流程和加载策略
ktr 通过自动生成的注册表管理模板、mock 数据和类型增强。本页深入讲解 .ktr/ 目录下三个文件的作用和运行机制。
.ktr/ 目录概览
.ktr/ ← 框架自动生成,类似 Next.js 的 .next/
├── template-registry.ts ← 路由 → 组件的映射表
├── mock-registry.ts ← mock 数据统一导出
└── registry-types.d.ts ← TypeScript 模块增强这三个文件由框架自动管理,不要手动编辑、不要提交到 Git、源码不要 import 它们。
将 .ktr/ 加入 .gitignore:
.ktr/template-registry.ts
作用
存储路由到组件的映射,供运行时加载使用。
生成内容
// .ktr/template-registry.ts(框架自动生成)
import type { } from '@karinjs/template-react'
// 导入所有模板组件(静态 import,路径不含扩展名)
import from '../ktr/template/hello/card/index'
import from '../ktr/template/user/profile/index'
import from '../ktr/template/stats/dashboard/index'
// 透传模板文件的具名导出
export * from '../ktr/template/hello/card/index'
export * from '../ktr/template/user/profile/index'
export * from '../ktr/template/stats/dashboard/index'
// 导出映射表(宽松标注,逐路由精确类型由 registry-types.d.ts 模块增强提供)
export const : <string, <any>> = {
'hello/card': ,
'user/profile': ,
'stats/dashboard':
}
// 辅助类型(用于运行时推导)
export type = typeof 路由命名规则
- Key:路由字符串,格式
<板块>/<模板> - Value:组件的 default export
- 变量名:
template_+ 路由路径(/替换为_)
// 示例:路由 user/profile/card
import from '../ktr/template/user/profile/card/index'
export const = {
'user/profile/card':
// ↑ 路由 key ↑ 变量名
}加载方式
开发态和生产态使用不同的加载策略:
export async function (: { : string; ?: string }) {
const { , } =
// 1. 开发态:.ktr 源文件优先(tsx 即时转译);
// 渲染器自身跑在下游 bundle 里时跳过这一步,直接用产物(见下方加载策略表)
const = .(, '.ktr', 'template-registry.ts')
if (.()) {
const = await import()
return .templates
}
// 2. 生产态:回退到打包产物
const = (, 'template-registry.js', )
if () {
const = await import()
return .templates
}
throw new ('未找到注册表:开发态请先运行 ktr sync,生产态请确保已打包')
}加载策略:
| 环境 | 加载路径 | 说明 |
|---|---|---|
| 开发态 | .ktr/template-registry.ts | tsx 动态导入,支持 HMR |
| 生产态 | 打包产物中的 template-registry.js | 按 bundledDir → package.json main/exports 入口目录 → 根目录扫描发现;渲染器自身在 bundle 里时直接使用,不看 .ktr |
mock-registry.ts
作用
统一导出所有 mock 数据(TS mock 和 JSON mock 清单)。
生成内容
// .ktr/mock-registry.ts(框架自动生成)
// ===== Part 1: TS mock 具名导出 =====
export * from '../ktr/template/hello/card/mock'
export * from '../ktr/template/user/profile/mock'
// 如果模板没有 mock.ts,跳过该行
// ===== Part 2: JSON mock 文件清单 =====
export const = {
'hello/card': ['captured.json', 'default.json', 'variant.json'],
'user/profile': ['basic.json']
} as
// 面板按「路由 + 文件名」定位 data/ 下的 JSON 文件TS mock vs JSON mock
| 特性 | TS mock | JSON mock |
|---|---|---|
| 位置 | mock.ts(与 index.tsx 同级) | data/*.json 子目录 |
| 导出方式 | 具名导出(export const basic = {...}) | 文件名字符串清单 |
| 类型安全 | ✅ 编译期校验(satisfies) | ❌ 运行时解析 |
| 面板可编辑 | ❌ 只读 | ✅ 可新建/编辑/删除 |
| 适合场景 | 固定示例,插件代码可复用 | 快速调试,多组对照数据 |
面板如何加载数据
开发面板通过 Mock API 读取数据:
export function () {
return async (: any, : any) => {
const = new (.url, 'http://localhost')
// GET /data?path=hello/card&name=basic
if (. === '/data') {
const = ..('path')!
const = ..('name')!
// 1. 优先读取 JSON 文件(面板可编辑)
const = .(., , 'data', `${}.json`)
if (.()) {
.json(.(.(, 'utf-8')))
return
}
// 2. 回退到 TS mock(面板只读)
const = await ()
if ([]) {
.json([])
return
}
.status(404).json({ : 'Data entry not found' })
}
}
}数据源优先级:JSON mock > TS mock
registry-types.d.ts
作用
通过 TypeScript 模块增强(Module Augmentation)注入类型,让 renderImage 获得:
- 路由补全:编辑器自动提示可用路由
- Data 类型推导:根据模板的 Props 类型推导 data 参数
生成内容
// .ktr/registry-types.d.ts(框架自动生成)
declare module '@karinjs/template-react/registry-types' {
interface ProjectRegistry {
'hello/card': typeof import('../ktr/template/hello/card/index').default
'user/profile': typeof import('../ktr/template/user/profile/index').default
'stats/dashboard': typeof import('../ktr/template/stats/dashboard/index').default
}
}
export {}这是一个环境声明文件(Ambient Declaration),TypeScript 编译器自动加载。
类型增强原理
框架的核心类型定义分两处:
// packages/core/src/registry-types.ts —— 只有增强位本身
// 用户项目通过模块增强填充这个接口
export interface ProjectRegistry {}
// packages/core/src/types/index.ts —— 其余类型定义在这里
// 从模板定义中提取 data 类型
export type <> = extends <infer > ? : never
// 宽松降级类型(向后兼容)
export type = <string, <any>>
// 注册表的实际类型(带降级)
export type = keyof ProjectRegistry extends never
? // 未增强时降级为宽松类型
: ProjectRegistry // 增强后为精确类型类型推导效果
有了类型增强,renderImage 自动获得智能提示:
// @filename: virtual.d.ts
import type { } from '@karinjs/template-react'
declare module '@karinjs/template-react/registry-types' {
interface ProjectRegistry {
'hello/card': <{
: string
: <{ : string; : string }>
}>
}
}
// @filename: utils/render.ts
import type { , } from '@karinjs/template-react'
import type { ImageElement } from 'node-karin'
// 插件胶水层封装的渲染函数(快速上手一节有完整实现)
export declare const : < extends keyof & string>(
: ,
: <[]>
) => <ImageElement[]>
// @filename: index.ts
import { } from './utils/render'
// ✅ 路由补全:输入 'hello/ 自动提示 'hello/card'
// ✅ data 类型推导:根据模板的 Props 自动推导
await ('hello/card', {
: 'Karin',
: [{ : '版本', : '2.0.0' }]
})
// ❌ 类型错误:路由不存在
await ('typo/card', {})
// ❌ 类型错误:data 类型不匹配
await ('hello/card', {
title: 123, // 应为 string items: 'wrong' // 应为 array})类型安全的最佳实践
在模板中显式定义 Data 接口:
// @filename: ./index.tsx
// ktr/template/hello/card/index.tsx
import { } from '@karinjs/template-react'
// 显式定义并导出 Data 接口
export interface HelloCardData {
: string
?: string
: <{
: string
: string | number
}>
}
// 泛型参数传入类型
export default <HelloCardData>({
: 'Hello 卡片',
: '简单的信息卡片',
: ({ , }) => {
// data 有完整类型推导
return (
< ="p-6 bg-surface rounded-lg">
< ="text-xl font-bold">{.}</>
{. && (
< ="text-muted">{.}</>
)}
< ="mt-4 space-y-2">
{..( => (
< ={.} ="flex justify-between">
< ="text-muted">{.}</>
< ="font-medium">{.}</>
</>
))}
</>
</>
)
}
})mock 文件引用该接口:
// ktr/template/hello/card/mock.ts
import type { } from './index'
export const = {
: 'Karin Template React',
: [{ : '渲染方式', : 'SSR HTML' }]
} satisfies 注册表生成流程
完整流程由 ensureConventions() 函数协调:
// conventions/registry.ts
import { } from 'fast-glob'
import * as from 'node:fs'
import * as from 'node:path'
export async function (: any) {
const { , } =
// 1. 扫描模板路由
const = await ()
// → ['hello/card', 'user/profile', 'stats/dashboard']
// 2. 生成 template-registry.ts
await (, , )
// 3. 生成 mock-registry.ts
await (, , )
// 4. 生成 registry-types.d.ts
await (, , )
}
async function (: string): <string[]> {
const = await ('**/index.tsx', {
: ,
: ['**/components/**', '**/_*/**']
})
return .(() => .(/\/index\.tsx$/, '')).()
}
async function (: string[], : string, : string) {
const : string[] = []
const : string[] = []
for (const of ) {
const = `template_${.(/\//g, '_')}`
// 生成路径不含扩展名
const = .(, , 'index')
.(`import ${} from '../${}'`)
.(` '${}': ${}`)
}
const = `import type { TemplateDef } from '@karinjs/template-react'
${.('\n')}
export const templates: Record<string, TemplateDef<any>> = {
${.(',\n')}
}
export type TemplateRegistry = typeof templates
`
.(.(, 'template-registry.ts'), )
}
async function (: string[], : string, : string) {
const : string[] = []
const : <string, string[]> = {}
for (const of ) {
const = .(, )
// 扫描 TS mock(生成路径不含扩展名)
const = .(, 'mock.ts')
if (.()) {
.(`export * from '../${.(/\.ts$/, '')}'`)
}
// 扫描 JSON mock,按路由聚合文件名字符串清单
const = .(, 'data')
if (.()) {
const = .().(() => .('.json'))
if (. > 0) {
[] =
}
}
}
let = '// TS mock exports\n'
if (. > 0) {
+= .('\n') + '\n\n'
}
+= '// JSON mock 文件清单\n'
+= 'export const mockDataFiles = {\n'
for (const [, ] of .()) {
+= ` '${}': [${.(() => `'${}'`).(', ')}],\n`
}
+= '} as const\n'
.(.(, 'mock-registry.ts'), )
}
async function (: string[], : string, : string) {
const : string[] = []
for (const of ) {
const = .(, , 'index')
.(` '${}': typeof import('../${}').default`)
}
const = `declare module '@karinjs/template-react/registry-types' {
interface ProjectRegistry {
${.('\n')}
}
}
export {}
`
.(.(, 'registry-types.d.ts'), )
}开发态 vs 生产态加载
开发态加载
开发服务器和沙盒直接加载 .ktr/*.ts 源码:
export async function () {
// 约定扫描模板路由,逐路由生成 /@fs/ 动态导入(vite 按需编译,HMR 即时生效)
const = await (.)
const = .(() => `['${.}', () => import('/@fs/${.(., .)}')]`)
// 生成虚拟模块代码
return `
import { createRoot } from 'react-dom/client'
const templateLoaders = [
${.(',\n ')}
]
// 沙盒逻辑(见开发面板架构一章)
`
}特点:
- 即时更新:修改模板代码,HMR 立即生效
- 无需打包:tsx 动态导入,开发体验流畅
生产态加载
Karin 插件从打包产物加载:
// 用户插件代码
import { } from '@karinjs/template-react'
// 定位包根(从 import.meta.url 向上找 package.json)
const = (import.meta.)
// 首次调用时内部加载注册表,查找顺序:
// 1. .ktr/template-registry.ts(开发态源文件,存在时永远优先)
// 2. bundledDir 显式指定的产物目录
// 3. package.json main 字段所在目录
// 4. package.json exports 主入口所在目录(纯 exports 包没有 main)
// 5. 根目录下一层目录扫描特点:
- 独立部署:注册表随包发布,无需源码
- 性能优化:打包后的 JS,加载更快
注册表失效排查
问题 1:路由未注册
现象:新建模板后,renderImage 提示路由不存在
排查步骤:
- 检查文件路径格式
✅ ktr/template/hello/card/index.tsx
❌ ktr/template/hello/card.tsx (裸文件不注册)
❌ ktr/template/hello/card/main.tsx (必须是 index)
❌ ktr/template/_draft/card/index.tsx (下划线目录不扫描)- 检查是否执行过
ktr sync
# 手动刷新注册表
pnpm ktr sync
# 或重启开发服务器
pnpm template- 查看生成的注册表
# 检查路由是否在列表中
cat .ktr/template-registry.ts | grep 'hello/card'问题 2:类型推导失效
现象:renderImage 没有路由补全或 data 类型为 any
排查步骤:
- 确认
registry-types.d.ts存在
ls -la .ktr/registry-types.d.ts- 检查 TypeScript 配置
{
"compilerOptions": {
"moduleResolution": "bundler", // 或 "node16"
"types": ["node"]
},
"include": [
"src/**/*",
".ktr/**/*" // 确保包含 .ktr 目录
]
}- 重启 TypeScript 服务器
VSCode: Cmd/Ctrl + Shift + P → "TypeScript: Restart TS Server"
问题 3:Mock 数据不显示
现象:开发面板数据卡片为空
排查步骤:
- 检查 mock 文件位置
✅ ktr/template/hello/card/mock.ts (TS mock)
✅ ktr/template/hello/card/data/default.json (JSON mock)
❌ ktr/template/hello/mock.ts (错误位置)- 检查 TS mock 导出方式
// ✅ 具名导出
export const = { : 'Hello' }
// ❌ 默认导出(不会被注册)
export default { : 'Hello' }- 查看生成的 mock 注册表
cat .ktr/mock-registry.ts问题 4:生产环境加载失败
现象:部署后 renderImage 提示"未找到注册表"
排查步骤:
- 确认已构建 CSS
pnpm build # 或 vite build
ls lib/style.css # 产物目录随你的打包器 outDir,这里以 lib/ 为例- 确认注册表在产物目录
# 检查打包产物(目录名同上)
ls lib/template-registry.js- 产物目录不在自动发现路径时,检查
bundledDir配置
export const = (import.meta., {
: 'dist' // 打包产物目录;不传时按 main/exports 入口和根目录扫描自动发现
})