json-render官网用组件目录约束 AI 生成的界面,阅读生成式 UI 的框架文档与示例。json-render.dev
教程:理解 json-render
先建立一个心智模型
json-render 不是「把 JSX 换一种写法」,它把 UI 的声明与实现分离:JSON 描述界面,React 负责实现。
- Schema 规定 JSON Spec 的结构与动态表达式格式,类似「语法」。
- Catalog 声明允许使用的组件、Props、Slots、Actions 和 Functions,类似「词汇」。
- Spec 是一次具体的页面结构,来自手写、仓库文件或 AI。
- Registry 将组件类型和动作名称映射到真正的 React 实现与 handlers。
- Dynamic Runtime 根据状态计算绑定、条件显隐、表单校验和动作执行。
- Renderer 结合 Spec、Registry 和运行时产生真实 React UI。
重要边界:Schema、Catalog、Registry、Spec 与 Renderer 是框架能力。后文的 Design Tokens、Base/Composite 分类、Page Controller、State / Action Contract 是 English Studio v2 在框架之上增加的工程架构,不是 json-render 官方强制要求的术语。
学习路线:一张任务卡片逐步变成可交互界面
下面沿用一个可以编辑标题、勾选子任务、展示进度、校验并保存的任务卡片,按官方能力从静态 Spec 逐步学习到动态渲染。
| 阶段 | 你要回答的问题 | 对应能力 |
|---|---|---|
| 描述 UI | 页面有哪些元素?JSON 如何组织? | Specs、Schemas |
| 限定能力 | AI 可以使用哪些组件和动作? | Catalog |
| 加入动态数据 | 输入框如何回写状态?如何计算进度? | Data Binding、Computed、Visibility |
| 处理交互 | 状态变化、输入校验、事件如何处理? | Watchers、Validation、Directives |
| 真实运行 | 组件和业务 handler 在哪里实现? | Registry、Renderer |
| AI 生成 | 如何增量生成页面或嵌入对话? | Streaming、Generation Modes |
安装
Installation。对等依赖:react ^19、zod ^4。
npm install @json-render/core @json-render/react zod
# 第 9 步用到的预置指令
npm install @json-render/directives- 现成组件:
@json-render/shadcn(36 个 Radix UI + Tailwind 组件)。本文自己写组件。
1. Specs:先写出想要的 JSON
Specs。扁平结构:root 是根元素 key,elements 是 key → 元素,子元素按 key 引用。
{
"root": "card",
"elements": {
"card": {
"type": "Card",
"props": { "title": "任务", "description": null },
"children": ["title-input", "save"]
},
"title-input": {
"type": "Input",
"props": { "label": "标题", "value": "写周报", "placeholder": null },
"children": []
},
"save": {
"type": "Button",
"props": { "label": "保存", "variant": "primary" },
"children": []
}
}
}- 元素字段:
type、props、children,另有visible、on、watch、repeat。 - 扁平结构便于流式补元素、按路径修改。
2. Schemas:Spec 遵循的语法
- Schema 是语法,Catalog 是词汇。
root + elements是 React 内置 Schema;core 也支持 A2UI、Adaptive Cards 等。
import { schema } from "@json-render/react/schema";3. Catalog:登记允许使用的东西
- 三类:
components、actions、functions。 - 组件:Zod
props(可选用.nullable())、slots、description,可选events、example。
import { defineCatalog } from "@json-render/core";
import { schema } from "@json-render/react/schema";
import { z } from "zod";
const check = z.object({
type: z.string(),
message: z.string(),
args: z.record(z.string(), z.unknown()).nullable(),
});
export const catalog = defineCatalog(schema, {
components: {
Card: {
props: z.object({ title: z.string(), description: z.string().nullable() }),
slots: ["default"],
description: "任务卡片容器",
},
Stack: {
props: z.object({ gap: z.enum(["sm", "md"]).nullable() }),
slots: ["default"],
description: "纵向排列子元素",
},
Text: {
props: z.object({ content: z.string(), tone: z.enum(["default", "muted"]).nullable() }),
description: "一段文字",
},
Input: {
props: z.object({
label: z.string(),
value: z.string(),
placeholder: z.string().nullable(),
checks: z.array(check).nullable(),
validateOn: z.enum(["change", "blur", "submit"]).nullable(),
}),
description: "单行输入框,value 用 $bindState 双向绑定",
},
Checkbox: {
props: z.object({ label: z.string(), checked: z.boolean() }),
description: "勾选框,checked 用 $bindState / $bindItem 绑定",
},
Button: {
props: z.object({ label: z.string(), variant: z.enum(["primary", "ghost"]).nullable() }),
events: ["press"],
description: "按钮,点击时发出 press 事件",
},
},
actions: {
saveTask: {
params: z.object({ title: z.string() }),
description: "保存任务标题",
},
countRemaining: {
params: z.object({}),
description: "重新计算未完成子任务数,写到 /meta/remaining",
},
},
functions: {
progressText: { description: "根据子任务数组返回“已完成 x / y”" },
},
});- 未登记的
type/ 动作:AI 看不到,校验报错。
4. Data Binding:读写状态
- 状态是 JSON 树,路径用 JSON Pointer(
/task/subtasks/0/done)。 - 初始状态:Spec 的
state,或 Provider 的initialState/store。
{
"task": {
"title": "写周报",
"due": "2026-10-16",
"subtasks": [
{ "id": "a", "label": "整理数据", "done": true },
{ "id": "b", "label": "写结论", "done": false }
]
},
"meta": { "remaining": 1 }
}绑定标题,用 repeat 渲染子任务:
{
"title-input": {
"type": "Input",
"props": { "label": "标题", "value": { "$bindState": "/task/title" }, "placeholder": null, "checks": null, "validateOn": null },
"children": []
},
"subtasks": {
"type": "Stack",
"props": { "gap": "sm" },
"repeat": { "statePath": "/task/subtasks", "key": "id" },
"children": ["subtask"]
},
"subtask": {
"type": "Checkbox",
"props": { "label": { "$item": "label" }, "checked": { "$bindItem": "done" } },
"children": []
}
}| 表达式 | 作用 | 使用位置 |
|---|---|---|
{ "$state": "/path" } |
只读一个状态值 | 任意 prop |
{ "$bindState": "/path" } |
双向绑定(value / checked / pressed) |
表单组件 |
{ "$item": "field" }、{ "$index": true } |
当前列表项字段、下标 | repeat 内 |
{ "$bindItem": "field" } |
双向绑定当前列表项字段 | repeat 内的表单组件 |
{ "$cond": …, "$then": …, "$else": … } |
条件取值 | 任意 prop |
repeat是元素顶层字段,不在props里。- 组件里:值在
props,写回路径在bindings,用useBoundProp(见第 10 节)。
5. Computed Values:派生展示值
$template:${/path}插值,缺失为空串。$computed:调用 Catalogfunctions中的函数,args可嵌套表达式。
{
"heading": {
"type": "Text",
"props": { "content": { "$template": "当前任务:${/task/title}" }, "tone": null },
"children": []
},
"progress": {
"type": "Text",
"props": {
"content": { "$computed": "progressText", "args": { "items": { "$state": "/task/subtasks" } } },
"tone": "muted"
},
"children": []
}
}实现在运行时传入:
export const functions = {
progressText: (args: Record<string, unknown>) => {
const items = (args.items as { done: boolean }[]) ?? [];
return `已完成 ${items.filter((i) => i.done).length} / ${items.length}`;
},
};6. Visibility:按条件显示
Visibility。visible 决定是否渲染:
{
"all-done": {
"type": "Text",
"props": { "content": "全部完成 🎉", "tone": null },
"children": [],
"visible": { "$state": "/meta/remaining", "eq": 0 }
}
}- 真值:
{ "$state": "/path" },"not": true取反。 - 比较:
eqneqgtgteltlte,每条一个;值可为$state。 - 组合:数组 = AND,
$or/$and可嵌套;true/false。 repeat内用$item/$index;放在repeat元素上可过滤列表(React)。
7. Watchers:状态变化时触发动作
Watchers。顶层 watch:状态路径 → 动作。子任务变化时重算剩余数:
{
"subtasks": {
"type": "Stack",
"props": { "gap": "sm" },
"repeat": { "statePath": "/task/subtasks", "key": "id" },
"watch": {
"/task/subtasks": { "action": "countRemaining" }
},
"children": ["subtask"]
}
}- 首次渲染不触发;按
===比较;多个动作顺序执行。 - 用户操作用
on,状态变化的副作用用watch。
8. Validation:校验输入
Validation。props 写 checks + validateOn,校验 $bindState 绑定的值:
{
"title-input": {
"type": "Input",
"props": {
"label": "标题",
"value": { "$bindState": "/task/title" },
"placeholder": "一句话说明任务",
"checks": [
{ "type": "required", "message": "标题不能为空", "args": null },
{ "type": "maxLength", "args": { "max": 40 }, "message": "最多 40 个字" }
],
"validateOn": "blur"
},
"children": []
}
}- 内置:
requiredemailminLengthmaxLengthpatternminmaxnumericurlmatches/equalTolessThangreaterThanrequiredIf。 - 自定义:登记到
functions,实现走customFunctions/validationFunctions。 - 组件用
useFieldValidation;提交用validateForm(结果写statePath,默认/formValidation):
{
"save": {
"type": "Button",
"props": { "label": "保存", "variant": "primary" },
"on": {
"press": [
{ "action": "validateForm", "params": { "statePath": "/form/check" } },
{ "action": "saveTask", "params": { "title": { "$state": "/task/title" } } }
]
},
"children": []
}
}- 校验失败不会跳过
saveTask,handler 要自己检查/form/check/valid。
9. Directives:自定义 $ 表达式
Directives。自定义 $ 表达式。
standardDirectives:$format$math$concat$count$truncate$pluralize$join。
{
"due": {
"type": "Text",
"props": {
"content": { "$format": "date", "value": { "$state": "/task/due" }, "style": "relative" },
"tone": "muted"
},
"children": []
}
}- 自定义:
defineDirective({ name, schema, resolve }),嵌套值用resolvePropValue。 - 同时传给 Provider 和
catalog.prompt({ directives })。 - 与内置重名会抛错。
10. Registry:给登记项提供实现
defineRegistry(catalog, { components, actions })→{ registry, handlers, executeAction }。- 组件上下文:
propschildrenslotsemitonbindingsloading。
import { defineRegistry, useBoundProp, useFieldValidation } from "@json-render/react";
import { catalog } from "./catalog";
export const { registry } = defineRegistry(catalog, {
components: {
Card: ({ props, children }) => (
<section className="card">
<h3>{props.title}</h3>
{props.description && <p>{props.description}</p>}
{children}
</section>
),
Stack: ({ props, children }) => <div className={`stack gap-${props.gap ?? "md"}`}>{children}</div>,
Text: ({ props }) => <p className={props.tone === "muted" ? "muted" : undefined}>{props.content}</p>,
Input: ({ props, bindings }) => {
const [value, setValue] = useBoundProp<string>(props.value, bindings?.value);
const { errors, validate } = useFieldValidation(bindings?.value ?? "", {
checks: props.checks ?? undefined,
validateOn: props.validateOn ?? undefined,
});
return (
<label>
{props.label}
<input
value={value ?? ""}
placeholder={props.placeholder ?? undefined}
onChange={(e) => setValue(e.target.value)}
onBlur={() => validate()}
/>
{errors.map((e) => <small key={e}>{e}</small>)}
</label>
);
},
Checkbox: ({ props, bindings }) => {
const [checked, setChecked] = useBoundProp<boolean>(props.checked, bindings?.checked);
return (
<label>
<input type="checkbox" checked={!!checked} onChange={(e) => setChecked(e.target.checked)} />
{props.label}
</label>
);
},
Button: ({ props, emit }) => (
<button data-variant={props.variant ?? "primary"} onClick={() => emit("press")}>
{props.label}
</button>
),
},
});动作实现两种放法:
defineRegistry的actions:(params, setState, state),setState收prev => next。- 直接传
handlers给 Provider(下例,配外部 store 按路径读写):
import { createStateStore } from "@json-render/react";
import initialState from "./task.state.json";
export const store = createStateStore(initialState);
export const handlers = {
countRemaining: () => {
const items = store.get("/task/subtasks") as { done: boolean }[];
store.set("/meta/remaining", items.filter((i) => !i.done).length);
},
saveTask: async (params: Record<string, unknown>) => {
if (!(store.get("/form/check") as { valid?: boolean })?.valid) return;
await fetch("/api/tasks", { method: "POST", body: JSON.stringify(params) });
},
};内置动作(免登记):
setState{ statePath, value }pushState{ statePath, value, clearStatePath? }removeState{ statePath, index }validateForm;React 另有push/pop
删除子任务:
{ "on": { "press": { "action": "removeState", "params": { "statePath": "/task/subtasks", "index": { "$index": true } } } } }绑定还可写 confirm、onSuccess、onError、preventDefault。
11. Renderers:把 Spec 渲染出来
- 目标:React、Vue、Svelte、Solid、React Native、Image、React PDF、Remotion、Ink 等。
- React 用
JSONUIProvider一次装好状态、可见性、校验、动作、函数、指令:
import { JSONUIProvider, Renderer } from "@json-render/react";
import { standardDirectives } from "@json-render/directives";
import spec from "./task.spec.json";
import { registry } from "./registry";
import { functions } from "./functions";
import { store, handlers } from "./controller";
export function TaskCard() {
return (
<JSONUIProvider
registry={registry}
store={store}
handlers={handlers}
functions={functions}
directives={standardDirectives}
>
<Renderer spec={spec} registry={registry} />
</JSONUIProvider>
);
}- 也可拆用
StateProviderVisibilityProviderValidationProviderActionProvider,或createRenderer(catalog, components)。 - 传了
store,initialState/onStateChange被忽略。
12. Streaming:让 AI 逐步生成
Streaming。SpecStream:每行一个 RFC 6902 patch(add remove replace move copy test)。
{"op":"add","path":"/root","value":"card"}
{"op":"add","path":"/elements/card","value":{"type":"Card","props":{"title":"任务","description":null},"children":["title-input"]}}
{"op":"add","path":"/elements/title-input","value":{"type":"Input","props":{"label":"标题","value":{"$bindState":"/task/title"},"placeholder":null,"checks":null,"validateOn":null},"children":[]}}- 服务端:
streamText({ system: catalog.prompt() })→toTextStreamResponse()。 - 客户端:
useUIStream。
const { spec, isStreaming, error, send, clear } = useUIStream({ api: "/api/generate" });
// <Renderer spec={spec} registry={registry} loading={isStreaming} />- 非 React:
createSpecStreamCompiler().push(chunk);一次性用compileSpecStream。 - 再次
send会中止上一个请求。
13. Generation Modes:独立界面与对话内界面
Generation Modes。patch 格式相同,区别是能否夹带文字:
| Standalone(默认) | Inline | |
|---|---|---|
| 输出 | 只有 JSONL | 先文字,再 JSONL;可以只回文字 |
| Prompt | catalog.prompt() |
catalog.prompt({ mode: "inline" }) |
| 服务端 | toTextStreamResponse() |
pipeJsonRender(result.toUIMessageStream()) |
| 客户端 | useUIStream |
useChat + useJsonRenderMessage(message.parts) |
| 场景 | Playground、表单/看板生成器 | 聊天助手、Copilot |
实战:English Studio v2 草图工作台
目标:设计完成的 UI 可以直接进入正式应用
这次实战不做一个新的低代码引擎,而是使用 json-render 构建 可复用、可校验、可交互、可交付 的组件与页面资产。
最重要的约定:
Spec 描述界面,TypeScript Page Controller 编排业务,类型化 State / Action Contract 连接两者。 草图与 macOS 使用完全相同的组件实现、Spec、Runtime 和 Renderer,只替换 Mock / Production 的数据及动作实现。
Intelligent 风格 · 可交互架构图
下图采用 OpenIntelligentUI 风格的卡片分层:可以切换「官方核心 / V2 分层 / 双端复用」、选择模块查看职责,并切换浅色与深色主题。
1. 五个核心边界,先分清职责
| 层次 | 负责 | 不负责 |
|---|---|---|
| Design Tokens | 统一颜色、字体、间距、圆角、动效与主题 | 页面数据与业务动作 |
| Component System | Base 和 Composite 的 React 实现及可复用 API | 跨页面的业务流程 |
| Schema / Catalog / Registry | JSON 语法、组件契约和真实实现映射 | 保存页面业务数据 |
| Page Spec / Dynamic Runtime | 布局、组件组合、绑定、显隐、计算值、基础交互 | 任意 JS、系统权限、复杂异步流程 |
| Page Controller / Contracts | 页面状态、Action handlers、请求重试、导航与宿主桥 | 重复编写组件 JSX 和 CSS |
动态行为是 Spec + 官方 Runtime 的共同能力,并非每个 Base Component 自行维护一份 Watchers 引擎。复杂的代码式 Composite 可以保留内部 React state,但页面只依赖其公开 Props 和事件。
2. Design Tokens:样式系统独立于组件
采用三层设计变量:
- Primitive:色板、字号和间距阶梯,仅提供原始值。
- Semantic:
text-primary、surface-raised、space-section等语义变量;主题切换主要调整此层的映射。 - Component:
button-height-md、card-padding等组件变量,优先引用语义变量。
组件实现只消费 Semantic / Component Tokens,不直接硬编码色值,也不在 Composite 中复制 Base 的 CSS。
:root {
--primitive-blue-600: #185fa5;
--color-action-primary: var(--primitive-blue-600);
--color-text-primary: #1a1a1a;
--radius-card: 12px;
--button-height-md: 36px;
}
[data-theme="dark"] {
--color-action-primary: #85b7eb;
--color-text-primary: #e8e6de;
}
.button { min-height: var(--button-height-md); color: var(--color-action-primary); }上面是变量依赖关系的简化示例。实际项目的
--color-text-primary、--radius-card等值也应由语义映射统一治理,不要把示例中的直接值扩散到组件源码。
3. Base 与 Composite:组件不是越小越好
| 类别 | 真实实现 | 使用场景 |
|---|---|---|
| Base Components | React / TSX(Radix UI + 自定义样式) | Button、Input、Card、Text |
| Declarative Composite | JSON Spec 模板 + 轻量展开适配器 | VocabularyCard、SettingSection 等稳定组合 |
| Code Composite | 独立 React / TSX | ListeningPlayer、复杂图表、拖拽与特殊交互 |
Base 的 Catalog 仅暴露稳定、语义化的设计 Props(variant、size、tone),不直接透传 Radix API。
声明式 Composite 可以组合 Base 或其他 Composite;模板展开器负责 Props / Slots / 事件映射、内部元素 ID 唯一性及各实例的局部状态隔离,最后仍交给官方 Renderer。
升级规则:出现大量条件分支、复杂内部状态或跨 Slot 通信,就改成 Code Composite;不继续扩展模板适配器,避免造出第二套框架。
例如 VocabularyCard 由 Card + Text + Button 组合;而 ListeningPlayer 完全可以是一整块 TSX 实现,内部使用音频 API、React Hooks 和自己专用的布局。它们都在 Catalog 登记,在 Registry 提供实现或适配器,使用相同 Tokens。
4. Catalog / Schema / Registry:声明、校验、实现分离
@json-render/react/schema ── 规定 Spec 的 JSON 结构
+
Catalog ── 组件 Props / Slots / Events、Actions / Functions
|
Registry ── Base React / Composite Adapter / Code Composite
|
Page Spec ── 实例化已登记组件
|
Renderer + Dynamic Runtime ── 真正渲染和交互这里只是关系示意:Schema + Catalog 共同校验 Spec;Registry 和 Runtime 在渲染时提供实现与状态,并非固定执行顺序。
组件的源代码独立存放在 TypeScript / TSX 文件里;Catalog 不是源码仓库。组件详情可以查看实现或声明式模板,页面工作台不显示整页 React 源码。
5. Spec 只声明 UI,Page Controller 负责业务
假设有一个单词学习页面:
{
"root": "page",
"elements": {
"page": {
"type": "Stack",
"props": { "gap": "md" },
"children": ["word", "submit"]
},
"word": {
"type": "VocabularyCard",
"props": {
"word": { "$state": "/learning/currentWord" },
"variant": "default"
},
"children": []
},
"submit": {
"type": "Button",
"props": { "label": "提交答案", "variant": "primary" },
"on": { "press": { "action": "submitAnswer", "params": {} } },
"children": []
}
}
}VocabularyCard、Stack、Button 需要在项目 Catalog 里存在;submitAnswer 也必须是已声明且可执行的 Action。这是按该项目 Catalog 设计的示意 Spec,不是 json-render 的内置组件清单。
Spec 可以声明布局、Props、绑定、条件显示、计算值、输入校验和 Action 触发。不要让 Spec 直接管理路由、键盘焦点、拖拽、虚拟列表、录音与系统权限,也不要用 Watchers 拼接多步异步请求、重试和取消。
6. State / Action Contract:草图与正式应用共享的边界
每个页面定义 Zod State Schema,对 Mock 数据和生产后端数据使用同一校验标准;每个 Action 声明名字、输入、成功输出和预期错误。
import { z } from "zod";
export const VocabularyPageState = z.object({
learning: z.object({
currentWord: z.string(),
answer: z.string(),
}),
status: z.object({
submitAnswer: z.object({
pending: z.boolean(),
error: z.string().nullable(),
}),
}),
});
export const SubmitAnswerParams = z.object({
answer: z.string(),
});
export const SubmitAnswerResult = z.object({
correct: z.boolean(),
});
export type VocabularyState = z.infer<typeof VocabularyPageState>;规定异步状态统一放入:
/status/<action>/pending/status/<action>/error
例如用户提交答案后,Controller 设置 pending,运行 Action,更新页面状态;出错时将可展示的错误信息写入 error。复杂错误码与重试策略留在 Controller。
Catalog 是可调用 Action 的白名单;即使用户或 AI 修改 Spec,也不能指定任意系统命令、网络地址或未经授权的业务操作。
7. Page Controller:只换业务实现,不换页面
建议通过统一接口在草图与应用中分别注入 Controller:
| 草图工作台 | macOS 正式应用 | |
|---|---|---|
| Spec | 同一份 vocabulary.page.json |
同一份 vocabulary.page.json |
| Registry / Renderer | 同一份 UI Package | 同一份 UI Package |
| State Schema | 同一份 Zod 契约 | 同一份 Zod 契约 |
| Page Controller | Mock State + Mock Actions | Production State + Production Actions |
| 业务连接 | 预设数据 / 本地模拟 | 宿主 Bridge → Rust 服务 |
例如 submitAnswer 在草图里返回模拟批改结果,在正式应用中通过 Tauri、WKWebView JS Bridge 或 Electron IPC 对接 Rust。Spec 不需要知道宿主通信细节。
Code Composite 可以管理高频内部状态,例如播放器波形或播放进度;需要跨组件共享时,再通过受控 Props、明确事件或 Controller 暴露必要信息,不直接读取组件内部 Hooks。
8. 草图工作台:四个入口
| 入口 | 主要内容 |
|---|---|
| Design Tokens | 设计变量、颜色、字体、主题和预览 |
| Base Components | 基础组件、Variants、Props、源码 |
| Composite Components | 声明式 / 代码式组件、依赖和实现 |
| Pages | 页面组件树、属性调整、实时预览、状态场景 |
Pages 可用 Mock 数据切换 正常 / 加载 / 空 / 错误 四类状态;工作台不显示独立 React 页面源码。
仓库是唯一事实源:工作台里的属性调整是临时预览,不自动改写设计资产;编码 Agent 在仓库里修改 Tokens、Components、Catalog、Registry、Spec 与契约,并提交 Git 变更。
9. 项目组织与 CI:让「一次设计」可验证
推荐逻辑组织(按实际仓库规模可以合并目录,不必机械复制):
ui/
design-tokens/
components/base/
components/composite/ # declarative / code
catalog/ registry/
specs/pages/
runtime/ # 官方运行时装配、模板适配
contracts/ # State Schema + typed Actions
controllers/ # mock / production
workbench/ # 设计和预览CI 至少检查:Spec 结构与 Catalog Props、组件引用与 Registry、Action 白名单、Composite 展开和实例隔离、Mock State Schema。使用 Catalog 的 example 与关键 Variants 做视觉回归,覆盖明暗主题、加载态和错误态。
如工作台与应用分别发布,需要让 Spec 声明版本与兼容的 Catalog 版本;共享单仓依赖时,优先通过相同版本的 UI Package 保证一致性。
10. AI 参与设计时的决策顺序
复用已有组件 → 调 Props / Variants → 扩展现有组件 → 新增声明式 Composite → 必要时升级代码式 Composite 或新增 Base → 更新 Catalog / Registry → 更新 State / Action Contract → 生成 Page Spec → 补 Mock 数据 → 通过 CI。
AI 不直接生成任意 React 代码进入 Spec,不为一个页面的颜色变化全局修改基础组件默认样式;新增 React 组件必须走仓库内的代码开发、校验与提交流程。
交付目标:Design once · Implement once · Reuse the same Spec.
草图预览和 macOS 运行的差别是注入的 Controller 与宿主桥,不是第二套页面组件实现。
资料:速查、常见坑与官方来源
Schema / Spec 字段速查
| 位置 | 字段 | 用途 |
|---|---|---|
| 顶层 | root、elements、state? |
根元素 ID、元素映射、初始状态 |
| 元素 | type、props、children、slots |
组件类型、属性、子节点和具名插槽 |
| 元素 | visible、repeat |
条件显示、列表重复 |
| 元素 | on、watch |
用户事件和状态变化触发 |
| Props | $state、$bindState、$item、$bindItem、$index |
状态读取、双向绑定、列表上下文 |
| Props | $computed、$template、Directives |
计算与格式化 |
| Action | action、params |
仅执行已登记的动作 |
常见问题
- Schema vs Catalog:前者约束 JSON 结构,后者约束组件及 Props;Zod 的页面 State Schema 又是 English Studio 自己定义的业务契约,三者职责不同。
- 组件样式漂移:Composite 复用 Base 实现与统一 Tokens;不要复制按钮 CSS 或随意覆盖内部样式。不同 Variants 带来的差异属于有意配置。
- 声明式模板是不是页面?:它可以是可复用的局部 Spec,但要对外定义 Props 和 Slots、在 Catalog 登记,且展开后仍遵循同一份 Schema;完整 Page 负责顶层组合。
- Watchers 不能代替 Controller:监听适合简单联动,复杂请求、重试、导航与系统能力应交给 TS 实现。
- 校验表单与校验 Spec 不一样:
validateForm不会自动终止后续所有 Action,需要由 Handler / Controller 处理提交前置条件。 - 多实例状态隔离:两个 VocabularyCard 不能共用相同的局部状态路径,声明式模板展开需要处理 ID 和 State Namespace。
- 实际 API 以安装版本为准:运行时 Provider、hooks、Action 参数以及生成模式可能随版本变化,不要把本文的项目架构示意当作可以直接复制的官方 API 代码。
官方学习资料
- 开始:Introduction · Installation · Quick Start
- 核心:Specs · Schemas · Catalog · Data Binding · Computed Values · Visibility · Watchers · Validation · Directives
- 渲染与生成:Renderers · Registry · Streaming · Generation Modes
- API:@json-render/core · @json-render/react · @json-render/shadcn · @json-render/directives
- 源码:vercel-labs/json-render
- UI 参考:Radix Primitives · shadcn/ui · OpenIntelligentUI
建议阅读顺序:Specs → Schemas → Catalog → Registry → Data Binding → Renderer → 其他动态能力 → 本文 English Studio 实战。