Skip to content
登录

json-render:从入门到实战

Updated View as Markdown
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 遵循的语法

Schemas。

  • Schema 是语法,Catalog 是词汇。
  • root + elements 是 React 内置 Schema;core 也支持 A2UI、Adaptive Cards 等。
import { schema } from "@json-render/react/schema";

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

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:读写状态

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:派生展示值

Computed Values。

  • $template:${/path} 插值,缺失为空串。
  • $computed:调用 Catalog functions 中的函数,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 取反。
  • 比较:eq neq gt gte lt lte,每条一个;值可为 $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": []
  }
}
  • 内置:required email minLength maxLength pattern min max numeric url matches/equalTo lessThan greaterThan requiredIf。
  • 自定义:登记到 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:给登记项提供实现

Registry。

  • defineRegistry(catalog, { components, actions }) → { registry, handlers, executeAction }。
  • 组件上下文:props children slots emit on bindings loading。
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 渲染出来

Renderers。

  • 目标: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>
  );
}
  • 也可拆用 StateProvider VisibilityProvider ValidationProvider ActionProvider,或 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:样式系统独立于组件

采用三层设计变量:

  1. Primitive:色板、字号和间距阶梯,仅提供原始值。
  2. Semantic:text-primary、surface-raised、space-section 等语义变量;主题切换主要调整此层的映射。
  3. 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 代码。

官方学习资料

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

Navigation

Type to search…

↑↓ navigate↵ selectEsc close