核心概念
约定式路由
文件扫描规则、路由生成流程和最佳实践
ktr 采用约定式路由设计:文件放到约定位置就自动注册,无需手写路由配置。
路由生成规则
基本规则
只有符合 <板块>/<模板>/index.tsx 模式的文件才会注册为路由:
ktr/template/
├── hello/
│ └── card/
│ └── index.tsx ✅ 注册为路由 hello/card
├── user/
│ ├── profile/
│ │ └── index.tsx ✅ 注册为路由 user/profile
│ └── card.tsx ❌ 裸文件不注册
└── stats/
└── dashboard/
├── index.tsx ✅ 注册为路由 stats/dashboard
└── components/
└── chart.tsx ❌ components 目录不扫描路由字符串格式:<板块>/<模板>,对应文件路径:ktr/template/<板块>/<模板>/index.tsx
扫描规则详解
框架使用 fast-glob 扫描模板目录,模式为 **/index.tsx,但有以下过滤规则:
| 规则 | 示例 | 说明 |
|---|---|---|
| 必须是 index 文件 | card/index.tsx ✅card.tsx ❌ | 目录深度不限,两级是惯例 |
| 忽略 components 目录 | card/components/badge.tsx ❌ | 模板内部组件 |
| 忽略下划线开头目录 | _shared/utils/index.tsx ❌ | 跨模板共享代码 |
// 框架内部扫描逻辑(简化)
import { } from 'fast-glob'
async function (: string) {
const = await ('**/index.tsx', {
: ,
: [
'**/components/**', // 忽略内部组件
'**/_*/**' // 忽略下划线目录
]
})
return .(() => {
// hello/card/index.tsx → hello/card
return .(/\/index\.tsx$/, '')
})
}路由生成流程
触发时机
注册表生成在以下场景自动触发:
ktr sync—— 手动刷新注册表ktr dev—— 启动开发服务器前- 构建插件 —— Vite/Rolldown 构建前(
buildStart钩子)
// ktr dev 启动流程
const = await ()
await () // ← 刷新注册表
const = await ()生成流程
完整流程分为 4 步:
1. 扫描文件系统
↓ discoverTemplateRoutes()
2. 生成 template-registry.ts
↓ generateTemplateRegistry()
3. 生成 mock-registry.ts
↓ generateMockRegistry()
4. 生成 registry-types.d.ts
↓ generateTypeAugmentation()步骤 1:扫描文件系统
// conventions/registry.ts
import { } from 'fast-glob'
import * as from 'node:path'
async function (: string) {
const = await ('**/index.tsx', {
: ,
: ['**/components/**', '**/_*/**']
})
// 转换为路由字符串
const = .(() => {
// 移除 /index.tsx 后缀
return .(/\/index\.tsx$/, '')
})
return .() // 按字母排序
}
// 结果示例:['hello/card', 'user/profile', 'stats/dashboard']步骤 2:生成 template-registry.ts
// 生成的 .ktr/template-registry.ts
import type { } from '@karinjs/template-react'
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'
export const : <string, <any>> = {
'hello/card': ,
'user/profile': ,
'stats/dashboard':
}
export type = typeof 路由 key 为原始路径字符串,value 为静态 import 的组件 default 导出。
步骤 3:生成 mock-registry.ts
// 生成的 .ktr/mock-registry.ts
// 导出所有 TS mock(具名导出)
export * from '../ktr/template/hello/card/mock'
export * from '../ktr/template/user/profile/mock'
// JSON mock 文件清单(保留 .json 后缀的文件名字符串数组)
export const = {
'hello/card': ['default.json', 'variant.json'],
'user/profile': ['basic.json']
} as 框架同时扫描每个模板目录下的:
mock.ts—— TS mock,直接 re-exportdata/*.json—— JSON mock,按路由聚合成文件名字符串清单
步骤 4:生成 registry-types.d.ts
// 生成的 .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
}
}这是 TypeScript 模块增强(Module Augmentation),让 renderImage 和 createTemplateRenderer 获得:
- 路由补全 —— 编辑器自动提示可用路由
- Data 类型推导 —— 根据模板的 Props 类型推导 data 参数
实际示例
示例 1:标准模板
ktr/template/
└── hello/
└── card/
├── index.tsx ← 模板组件
├── mock.ts ← TS mock
├── components/
│ └── badge.tsx ← 内部组件(不扫描)
└── data/
├── default.json ← JSON mock
└── captured.json ← 捕获数据// ktr/template/hello/card/index.tsx
import { } from '@karinjs/template-react'
import type { HelloCardData } from './mock'
export default <HelloCardData>({
: 'Hello 卡片',
: '简单的信息卡片',
: ({ }) => (
< ="p-6 bg-surface rounded-lg">
< ="text-xl font-bold">{.}</>
< ="mt-4 space-y-2">
{..( => (
< ={.} ="flex justify-between">
< ="text-muted">{.}</>
< ="font-medium">{.}</>
</>
))}
</>
</>
)
})// ktr/template/hello/card/mock.ts
import type { } from './index'
export const = {
: 'Karin Template React',
: [
{ : '渲染方式', : 'SSR HTML' },
{ : '样式方案', : 'Tailwind CSS v4' }
]
} satisfies
export const = {
: '空状态',
: []
} satisfies 注册结果:
- 路由:
hello/card - TS mock:
basic、empty(面板显示为只读数据源) - JSON mock:
default、captured(面板可编辑)
示例 2:共享代码组织
跨模板的工具函数和组件应放在下划线开头的目录:
ktr/template/
├── _shared/ ← 不会被扫描
│ ├── components/
│ │ ├── avatar.tsx
│ │ └── badge.tsx
│ └── utils/
│ └── format.ts
├── user/
│ └── profile/
│ └── index.tsx ← 可 import ../_shared
└── hello/
└── card/
└── index.tsx ← 可 import ../_shared// ktr/template/user/profile/index.tsx
import { } from '@karinjs/template-react'
import { } from '../../_shared/components/avatar'
import { } from '../../_shared/utils/format'
export default ({
: '用户资料',
: ({ }) => (
<>
< src={.avatar} />
<>{(.joinedAt)}</p>
</div>
)
})示例 3:多级板块组织
路由深度不限——两级 <板块>/<模板> 只是惯例,更深的目录同样注册(如 demo/nested/deep/index.tsx → 路由 demo/nested/deep):
ktr/template/
├── user/
│ ├── profile/
│ │ └── index.tsx → 路由 user/profile
│ ├── card/
│ │ └── index.tsx → 路由 user/card
│ └── badge/
│ └── index.tsx → 路由 user/badge
└── stats/
├── chart/
│ └── index.tsx → 路由 stats/chart
└── table/
└── index.tsx → 路由 stats/table开发面板会按板块(第一级目录名)分组显示模板列表。
最佳实践
1. 命名约定
- 板块名:小写,用
-分隔单词(如user-profile、stats-chart) - 模板名:小写,描述性名词(如
card、profile、dashboard) - 避免:中文路径、特殊字符、空格
✅ 推荐
ktr/template/user-info/profile/index.tsx → user-info/profile
ktr/template/stats/daily-chart/index.tsx → stats/daily-chart
❌ 不推荐
ktr/template/用户信息/个人资料/index.tsx
ktr/template/stats/Daily Chart/index.tsx2. 目录结构建议
单个模板的推荐结构:
ktr/template/hello/card/
├── index.tsx ← 模板入口,默认导出 defineTemplate
├── mock.ts ← TS mock,具名导出多个示例
├── components/ ← 内部组件(只被本模板使用)
│ ├── header.tsx
│ └── footer.tsx
├── hooks/ ← 自定义 hooks(可选)
│ └── use-animation.ts
└── data/ ← JSON mock
├── default.json
├── variant.json
└── captured.json3. 类型安全实践
在模板文件中定义 Data 接口,mock 文件引用:
// @filename: ./index.tsx
// ktr/template/hello/card/index.tsx
import { } from '@karinjs/template-react'
export interface HelloCardData {
: string
?: string
: <{
: string
: string | number
}>
}
export default <HelloCardData>({
: 'Hello 卡片',
: ({ }) => {
// data 有完整类型推导
return <>{.}</>
}
})// ktr/template/hello/card/mock.ts
import type { } from './index'
// satisfies 确保类型匹配,同时保留字面量类型
export const = {
: 'Karin Template React',
: [{ : '渲染方式', : 'SSR HTML' }]
} satisfies
// 或使用辅助函数
import { } from '@karinjs/template-react'
export const = <>({
: '高级示例',
: '带副标题',
: [
{ : '版本', : '2.0.0' },
{ : '星标', : 1024 }
]
})4. 何时刷新注册表
注册表会在以下情况自动刷新:
- ✅
ktr dev启动时 - ✅ 开发面板每次请求模板列表前(自动
ensureTemplateRegistry重扫,模板增删即时生效) - ✅
ktr build构建时 - ✅ Vite/Rolldown 插件
buildStart钩子
开发态新建、删除、重命名模板目录,或增删 mock.ts / data/*.json,都无需手动 ktr sync,也无需重启 dev server——面板刷新页面即可看到变化。ktr sync 用于不走 dev server 和构建流程的场景(比如只想要 .ktr 类型增强时):
pnpm ktr sync5. 调试注册表
注册表文件在 .ktr/ 目录,可直接查看:
# 查看注册的路由
cat .ktr/template-registry.ts
# 查看 mock 导出
cat .ktr/mock-registry.ts
# 查看类型增强
cat .ktr/registry-types.d.ts如果发现路由未注册或类型不对,检查:
- 文件路径是否符合
<板块>/<模板>/index.tsx - 是否在
components/或_*目录内 - 文件扩展名是否为
.tsx(只扫描index.tsx) - 是否执行过
ktr sync