---
title: "json-render：从入门到实战"
description: "从 Schema、Catalog、Spec、Registry 和动态运行时学会 json-render，再实践 Design Tokens、Base/Composite Components、Page Controller 与 macOS 双端复用。"
---

> Documentation Index
> Fetch the complete documentation index at: https://docs.chengchencode.com/llms.txt
> Use this file to discover all available pages before exploring further.

# json-render：从入门到实战

## 教程：理解 json-render

### 先建立一个心智模型

**json-render 不是「把 JSX 换一种写法」**，它把 UI 的<mark>声明与实现分离</mark>：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](https://json-render.dev/docs/installation)。对等依赖：`react ^19`、`zod ^4`。

```bash
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](https://json-render.dev/docs/specs)。扁平结构：`root` 是根元素 key，`elements` 是 key → 元素，子元素按 key 引用。

```json title="task.spec.json"
{
  "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 遵循的语法

[Schemas](https://json-render.dev/docs/schemas)。

- Schema 是语法，Catalog 是词汇。
- `root + elements` 是 React 内置 Schema；core 也支持 A2UI、Adaptive Cards 等。

```ts
import { schema } from "@json-render/react/schema";
```

### 3. Catalog：登记允许使用的东西

[Catalog](https://json-render.dev/docs/catalog)。

- 三类：`components`、`actions`、`functions`。
- 组件：Zod `props`（可选用 `.nullable()`）、`slots`、`description`，可选 `events`、`example`。

```ts title="catalog.ts"
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：读写状态

[Data Binding](https://json-render.dev/docs/data-binding)。

- 状态是 JSON 树，路径用 JSON Pointer（`/task/subtasks/0/done`）。
- 初始状态：Spec 的 `state`，或 Provider 的 `initialState` / `store`。

```json title="初始状态"
{
  "task": {
    "title": "写周报",
    "due": "2026-10-16",
    "subtasks": [
      { "id": "a", "label": "整理数据", "done": true },
      { "id": "b", "label": "写结论", "done": false }
    ]
  },
  "meta": { "remaining": 1 }
}
```

绑定标题，用 `repeat` 渲染子任务：

```json title="task.spec.json（节选）"
{
  "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：派生展示值

[Computed Values](https://json-render.dev/docs/computed-values)。

- `$template`：`${/path}` 插值，缺失为空串。
- `$computed`：调用 Catalog `functions` 中的函数，`args` 可嵌套表达式。

```json
{
  "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": []
  }
}
```

实现在运行时传入：

```ts title="functions.ts"
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](https://json-render.dev/docs/visibility)。`visible` 决定是否渲染：

```json
{
  "all-done": {
    "type": "Text",
    "props": { "content": "全部完成 🎉", "tone": null },
    "children": [],
    "visible": { "$state": "/meta/remaining", "eq": 0 }
  }
}
```

- 真值：`{ "$state": "/path" }`，`"not": true` 取反。
- 比较：`eq` `neq` `gt` `gte` `lt` `lte`，每条一个；值可为 `$state`。
- 组合：数组 = AND，`$or` / `$and` 可嵌套；`true` / `false`。
- `repeat` 内用 `$item` / `$index`；放在 `repeat` 元素上可过滤列表（React）。

### 7. Watchers：状态变化时触发动作

[Watchers](https://json-render.dev/docs/watchers)。顶层 `watch`：状态路径 → 动作。子任务变化时重算剩余数：

```json
{
  "subtasks": {
    "type": "Stack",
    "props": { "gap": "sm" },
    "repeat": { "statePath": "/task/subtasks", "key": "id" },
    "watch": {
      "/task/subtasks": { "action": "countRemaining" }
    },
    "children": ["subtask"]
  }
}
```

- 首次渲染不触发；按 `===` 比较；多个动作顺序执行。
- 用户操作用 `on`，状态变化的副作用用 `watch`。

### 8. Validation：校验输入

[Validation](https://json-render.dev/docs/validation)。props 写 `checks` + `validateOn`，校验 `$bindState` 绑定的值：

```json
{
  "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": []
  }
}
```

- 内置：`required` `email` `minLength` `maxLength` `pattern` `min` `max` `numeric` `url` `matches`/`equalTo` `lessThan` `greaterThan` `requiredIf`。
- 自定义：登记到 `functions`，实现走 `customFunctions` / `validationFunctions`。
- 组件用 `useFieldValidation`；提交用 `validateForm`（结果写 `statePath`，默认 `/formValidation`）：

```json
{
  "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](https://json-render.dev/docs/directives)。自定义 `$` 表达式。

- `standardDirectives`：`$format` `$math` `$concat` `$count` `$truncate` `$pluralize` `$join`。

```json
{
  "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：给登记项提供实现

[Registry](https://json-render.dev/docs/registry)。

- `defineRegistry(catalog, { components, actions })` → `{ registry, handlers, executeAction }`。
- 组件上下文：`props` `children` `slots` `emit` `on` `bindings` `loading`。

```tsx title="registry.tsx"
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 按路径读写）：

```ts title="controller.ts"
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`

删除子任务：

```json
{ "on": { "press": { "action": "removeState", "params": { "statePath": "/task/subtasks", "index": { "$index": true } } } } }
```

绑定还可写 `confirm`、`onSuccess`、`onError`、`preventDefault`。

### 11. Renderers：把 Spec 渲染出来

[Renderers](https://json-render.dev/docs/renderers)。

- 目标：React、Vue、Svelte、Solid、React Native、Image、React PDF、Remotion、Ink 等。
- React 用 `JSONUIProvider` 一次装好状态、可见性、校验、动作、函数、指令：

```tsx title="TaskCard.tsx"
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>
  );
}
```

- 也可拆用 `StateProvider` `VisibilityProvider` `ValidationProvider` `ActionProvider`，或 `createRenderer(catalog, components)`。
- 传了 `store`，`initialState` / `onStateChange` 被忽略。

### 12. Streaming：让 AI 逐步生成

[Streaming](https://json-render.dev/docs/streaming)。SpecStream：每行一个 RFC 6902 patch（`add` `remove` `replace` `move` `copy` `test`）。

```text
{"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`。

```tsx
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](https://json-render.dev/docs/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 分层 / 双端复用」、选择模块查看职责，并切换浅色与深色主题。

<iframe
  src="/notes/json-render/intelligent-architecture.html?view=v2"
  title="json-render 与 English Studio v2 的交互式架构图"
  loading="lazy"
  style="display:block;width:100%;height:930px;border:0;border-radius:18px;background:transparent;"
></iframe>

[独立打开架构图（适合全屏查看）](/notes/json-render/intelligent-architecture.html?view=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：样式系统独立于组件

采用三层设计变量：

1. **Primitive**：色板、字号和间距阶梯，仅提供原始值。
2. **Semantic**：`text-primary`、`surface-raised`、`space-section` 等语义变量；主题切换主要调整此层的映射。
3. **Component**：`button-height-md`、`card-padding` 等组件变量，优先引用语义变量。

组件实现<mark>只消费 Semantic / Component Tokens</mark>，不直接硬编码色值，也不在 Composite 中复制 Base 的 CSS。

```css title="tokens.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：声明、校验、实现分离

```text
@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 负责业务

假设有一个单词学习页面：

```json title="vocabulary.page.json（简化示意）"
{
  "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 声明名字、输入、成功输出和预期错误。

```ts title="vocabulary.contract.ts（项目约定）"
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 是<mark>可调用 Action 的白名单</mark>；即使用户或 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。<mark>Spec 不需要知道宿主通信细节</mark>。

Code Composite 可以管理高频内部状态，例如播放器波形或播放进度；需要跨组件共享时，再通过受控 Props、明确事件或 Controller 暴露必要信息，不直接读取组件内部 Hooks。

### 8. 草图工作台：四个入口

| 入口 | 主要内容 |
| --- | --- |
| **Design Tokens** | 设计变量、颜色、字体、主题和预览 |
| **Base Components** | 基础组件、Variants、Props、源码 |
| **Composite Components** | 声明式 / 代码式组件、依赖和实现 |
| **Pages** | 页面组件树、属性调整、实时预览、状态场景 |

Pages 可用 Mock 数据切换 **正常 / 加载 / 空 / 错误** 四类状态；工作台**不显示独立 React 页面源码**。

<mark>仓库是唯一事实源</mark>：工作台里的属性调整是临时预览，不自动改写设计资产；编码 Agent 在仓库里修改 Tokens、Components、Catalog、Registry、Spec 与契约，并提交 Git 变更。

### 9. 项目组织与 CI：让「一次设计」可验证

推荐逻辑组织（按实际仓库规模可以合并目录，不必机械复制）：

```text
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](https://json-render.dev/docs) · [Installation](https://json-render.dev/docs/installation) · [Quick Start](https://json-render.dev/docs/quick-start)
- **核心**：[Specs](https://json-render.dev/docs/specs) · [Schemas](https://json-render.dev/docs/schemas) · [Catalog](https://json-render.dev/docs/catalog) · [Data Binding](https://json-render.dev/docs/data-binding) · [Computed Values](https://json-render.dev/docs/computed-values) · [Visibility](https://json-render.dev/docs/visibility) · [Watchers](https://json-render.dev/docs/watchers) · [Validation](https://json-render.dev/docs/validation) · [Directives](https://json-render.dev/docs/directives)
- **渲染与生成**：[Renderers](https://json-render.dev/docs/renderers) · [Registry](https://json-render.dev/docs/registry) · [Streaming](https://json-render.dev/docs/streaming) · [Generation Modes](https://json-render.dev/docs/generation-modes)
- **API**：[@json-render/core](https://json-render.dev/docs/api/core) · [@json-render/react](https://json-render.dev/docs/api/react) · [@json-render/shadcn](https://json-render.dev/docs/api/shadcn) · [@json-render/directives](https://json-render.dev/docs/api/directives)
- **源码**：[vercel-labs/json-render](https://github.com/vercel-labs/json-render)
- **UI 参考**：[Radix Primitives](https://www.radix-ui.com/primitives) · [shadcn/ui](https://ui.shadcn.com/) · [OpenIntelligentUI](https://github.com/CopilotKit/OpenIntelligentUI)

建议阅读顺序：**Specs → Schemas → Catalog → Registry → Data Binding → Renderer → 其他动态能力 → 本文 English Studio 实战**。

Source: https://docs.chengchencode.com/notes/json-render/index.mdx
