Generative UI 的 Vue 端落地:白名单 Schema 渲染与三种架构选型
让模型直接生成模板/JSX 并渲染,只能停在 demo 阶段,上生产我直接否决。XSS 只是最明显的问题——模板编译器等于把执行边界交到模型手里;更现实的是设计系统约束不住输出,测试也不知道该断言什么。Vue 这边也没有额外优势:SFC 模板本来就该编译,运行时喂一段模型生成的模板,要么引编译器,要么做一套作用域隔离,收益是负的。
生产可行的主流路径是另一种:模型输出受约束的结构化 JSON,组件类型必须是组件目录(catalog)里的白名单成员;前端先做运行时校验,再按白名单渲染。UI 的最小编排单元是“目录里的组件”,不是 DOM/HTML。
Vue 端落地:Registry + 动态组件
先维护一个组件注册表,模型输出里只允许出现这里的 type:
// catalog/registry.ts
import { defineAsyncComponent } from 'vue'
export const registry = {
stat: defineAsyncComponent(() => import('./catalog/StatCard.vue')),
chart: defineAsyncComponent(() => import('./catalog/LineChart.vue')),
table: defineAsyncComponent(() => import('./catalog/DataTable.vue')),
}
渲染器把 schema 节点映射成组件:
import { computed } from 'vue'
import { registry } from './catalog/registry'
import { validateSchemaNode, sanitizeProps } from './validation'
const props = defineProps()
const validated = computed(() => validateSchemaNode(props.node))
const view = computed(() =>
validated.value.ok ? (registry[validated.value.node.type] ?? null) : null
)
一个 Vue 侧具体的坑:`` 别直接吃 node.type 字符串。直接传字符串等于让 Vue 走动态组件注册名解析,没命中只给警告,而且白名单门禁形同虚设。先过 registry 这一层,拿不到就是无效产出,走回退。defineAsyncComponent 在这里顺带解决了按需加载:目录再大,首屏只拉渲染到的组件 chunk。
三种渲染架构怎么选
Controlled 受控渲染:组件全部由前端提前实现,模型能做的被压缩到“选哪个组件 + 填哪些 props”。像素级可控,适合官网、活动页这类视觉和文案都不能漂移的 surface。代价是维护成本:每新增一个组件都要人重新对齐语义、做视觉回归;预建组件到 25 个左右的规模后成本陡增,所以它只适合窄场景,不适合当全站架构。
Declarative 声明式渲染:agent 返回 schema,前端把 schema node 映射到 catalog 里的组件。适合 use case 多、过了原型期、开始在意 token 成本的产品。UI 的细节沉淀在组件里,模型输出里只需要出现最小 schema,一个 tool 能覆盖一大片 UI 变体,规模化更平坦。
但它有两个代价:layout 的决定权在模型手里;输出会随 catalog 版本和模型行为漂移。视觉上“可用”,但难 pixel-perfect。法律披露、营销类对呈现精度敏感的页面,不要用这个模式。
Open-ended 开放渲染:模型直接产出 raw HTML,扔进 sandbox/iframe。适合用完即弃的一次性可视化,没有 catalog 认证成本,出图快。但不要把它当主 surface 反复迭代,第二次 ship 就会从各处开始脆。
不管选哪种,链路保持不变:Tool 层先从可信数据源取数 → 模型只生成展示和低风险交互的 UI JSON → 有副作用的操作回到用户确认,由受信代码执行。模型输出里不应该直接携带“删除全部”这类动作。
安全边界与回退路径
安全清单按下面这几条执行,缺一条都不进生产:
- 渲染层没有
v-html,schema 里不接受 raw HTML 节点。 - props 只放 catalog 登记过的白名单字段,不能把模型给的整个
props对象直接v-bind;文本节点交给 Vue 默认插值转义。 - 跳转 URL、图片 src 都要过域名白名单,不允许模型发任意链接。
- Button action 是枚举,从“查看详情”“翻页”这类呈现型动作里选;
deleteAll不在枚举里。 - 限制 schema 的节点数量和嵌套深度,渲染前执行。
- 服务端按当前会话和角色再校验一次。UI 里没显示按钮不叫权限控制,只是体验设计。
Schema 校验是运行时门控,不是构建时检查。模型输出本质是概率性的,类型对了也可能出现 catalog 内不合法的组合(比如表格嵌套在图表里)。所以 validateSchemaNode 要挂在每次渲染路径上,不能只在生成后校验一次。校验失败时回退到产品表面的降级态:一段基础文案卡片 + 重新生成按钮,而不是错误页。用户不该感知到“模型这次生成坏了”,只该觉得这轮结果不合用。
Catalog 即契约
组件进 catalog 前先做一次性认证:可访问性、语义审查、危险渲染模式排查。模型自由度被锁在认证过的范围内,它永远无法输出一个没进目录的组件。Catalog 的 props 要整理成可 JSON 序列化、文档化的 schema,不要把 Vue 组件的 runtime props 直接丢给模型猜。
这个语境下,设计系统会从“人类松散遵循的设计指南”变成“模型必须遵守的类型系统”:颜色、间距、可用组件、交互动作都变成可枚举、可校验的约束,而不是靠 prompt 里的几段劝导。模型能选的本来就有限,设计约束就从“说服”变成了机制。