前言
大家在做后台系统开发时,有没有遇到过这样的场景?运营同事跑过来说:“我想要个简单的库存报表,这就这几个字段,能不能马上弄好?”
这时候,你看着手头堆积如山的需求,心里可能在想:要是能直接跟电脑说一句“给我个库存表”,界面就能自己长出来该多好啊!
这就是 NL2UI (Natural Language to User Interface) 的终极梦想——用自然语言直接生成界面。但说实话,让 AI 直接写 Vue 代码稍微有点“吓人”,代码质量不可控不说,改起来还费劲。
今天,我们换个思路。我们不追求一步到位的“全自动”,而是基于 华为云 DevUI MateChat 组件,打造一个“受限但绝对可靠”的 UI 生成引擎。
我们会用一套自己定义的 JSON DSL(领域特定语言) 作为中间层,让 AI 做“填空题”,而不是“作文题”。这样既利用了 AI 的理解能力,又保证了生成的界面是 100% 可用的。
为了方便大家验证,我把这个引擎的完整代码都开源了。大家可以去 GitCode 仓库 gitcode.com/kaminono/Ma… 看看源码,或者直接点这个 mate-chat-nl-2-ui-engine-components.vercel.app/ 在线体验一下“说话变界面”。
一、 架构思考:为什么我们需要一个“中间商”?
在动手写代码之前,我们得先定个基调。要实现 NL2UI,我们面临两个选择:是让 AI 直接吐出 Vue 代码,还是让它生成一个 JSON 数据?
我们坚定地选择了后者。直接生成代码就像是“开盲盒”,你永远不知道 AI 会不会引入什么奇怪的依赖或者写出有安全漏洞的逻辑。
而生成 JSON DSL 就稳妥多了。我们把 DevUI 的组件——比如 d-card、d-form、d-chart——看作是乐高积木。我们只允许 AI 挑选这些积木来搭建页面。
我们可以把整个流程看作一个流水线:
- Input: 用户在 MateChat 输入自然语言。
- Reasoning: LLM 基于 System Prompt 进行意图识别,转化为标准 JSON。
- Parser: 前端引擎拦截消息,正则清洗数据,校验 JSON 合法性。
- Render: 递归组件读取 JSON,动态映射为 DevUI 组件。
这种“控制反转”的设计,是我们保证系统高可靠性的基石。
二、 核心实现:给 AI 立规矩,教它说“DSL”
搞定了架构,我们来看看核心代码是怎么实现的。这个引擎的“大脑”在 useNlParser.ts 文件里。
我们需要利用 Prompt Engineering(提示词工程),把我们的 DSL 语法“喂”给大模型。我们得明确告诉它:你只能用白名单里的组件,输出格式必须是标准的 JSON。
看看这段真实的代码,我们在 System Prompt 里做了非常严格的约束:
// playground/src/nl2ui-engine/composables/useNlParser.tsconst systemPrompt = `
你是一个专业的前端 UI 构建专家。你的任务是将用户的自然语言需求转换为特定的 UI DSL (JSON 格式)。
### 🔴 严禁使用不存在的组件!只能使用以下白名单:
1. 布局: "d-row", "d-col"
2. 容器: "d-card" (必须包含 children), "d-form" (children 必须是 d-form-item)
3. 表单项: "d-form-item" (props: label), "d-input", "d-select", "d-button"
4. 图表: "simple-stat", "simple-chart"
### 输出格式规范 (JSON)
必须严格遵守以下 JSON 结构,不要包含 markdown 代码块标记:
{
"page": { "title": "页面标题", "layout": "grid" | "default" },
"components": [
{
"component": "d-card",
"props": { "title": "卡片标题" },
"children": [ ... ]
}
]
}
`;
通过这种方式,无论用户怎么描述需求,AI 最终吐出来的都是我们能看懂、能渲染的标准数据。这就像是给 AI 戴上了“紧箍咒”,让它的创造力在规则的轨道上运行。
在实际开发中,LLM 经常会在返回的 JSON 外面包裹 Markdown 标记(如 ```json ... ```)。如果不处理,JSON.parse 必挂。 我们在解析层做了一层“清洗”:
// 解析逻辑片段
const parseResponse = (content: string) => {
// 1. 利用正则提取最外层的 {} 内容,去除废话和 markdown 符号
const jsonMatch = content.match(/\{[\s\S]*\}/);
if (!jsonMatch) return null;
try {
return JSON.parse(jsonMatch[0]);
} catch (e) {
console.error("JSON 解析失败,AI 生成了非法格式", e);
// 这里甚至可以触发一个重试机制
return null;
}
}
三、 渲染引擎:把 JSON 变现为 DevUI 组件
拿到了 JSON 数据,下一步就是把它变成真实的界面。我们在 DslRenderer.vue 里实现了一个递归渲染器。
这个组件的设计非常巧妙,它利用了 Vue 的 h() 函数和 defineAsyncComponent。我们建立了一个组件注册表,按需加载 DevUI 的组件。
这里有个关键点:对于 AI 可能产生的“幻觉”(比如生成了不存在的组件),我们做了兜底处理。
// playground/src/nl2ui-engine/components/DslRenderer.vue// 1. 建立组件白名单映射const componentRegistry: Record<string, any> = {
'd-card': defineAsyncComponent(() => import('vue-devui/card')),
'd-form': defineAsyncComponent(() => import('vue-devui/form')),
'd-input': defineAsyncComponent(() => import('vue-devui/input')),
// ... 其他组件
};
// 2. 核心渲染函数const renderNode = (node: any): any => {
// 兜底策略:如果 AI 生成了纯文本,直接渲染文本if (typeof node === 'string') return String(node);
let Component = componentRegistry[node.component];
// 错误处理:遇到未知组件,渲染一个红框提示,而不是让页面崩溃if (!Component) {
return h('div', { style: 'border: 1px dashed red;' }, `[未知: ${node.component}]`);
}
// 递归渲染子节点const children = node.children?.map(renderNode);
return h(Component, node.props, { default: () => children });
};
这段代码保证了渲染器的健壮性。哪怕 AI 偶尔“发疯”,我们的页面也不会白屏,开发者一眼就能看出是哪里出了问题。
递归渲染树 (The Recursive Magic)是引擎最精妙的地方。因为 UI 结构是树形的(Card 里有 Row,Row 里有 Col,Col 里有 Button),我们的渲染函数必须是递归的。
const renderNode = (node: any): any => {
if (typeof node === 'string') return node;
const Component = componentRegistry[node.component];
if (!Component) return h('div', { style: 'color:red' }, `[未知组件: ${node.component}]`);
// 核心:处理 props 和 children
// 1. 透传 AI 生成的属性 (如 label, placeholder)
const props = { ...node.props };
// 2. 递归构建子节点
const children = node.children
? { default: () => node.children.map(renderNode) } // 插槽形式传递子节点
: null;
return h(Component, props, children);
};
这段代码仅用十几行,就实现了理论上无限嵌套的 UI 构建能力。
四、 价值闭环:不仅能看,还能“带走”
如果只能在预览里看,那这个工具充其量只是个玩具。为了让它真正产生价值,我们必须实现“从对话到源码”的闭环。
试想一下,你让 AI 生成了一个复杂的表单,觉得效果不错。这时候,你肯定不想照着预览图再去手写一遍代码吧?
所以,我们开发了 useCodeGenerator.ts。它能把当前的 JSON DSL 逆向编译成标准的 Vue SFC(单文件组件)代码。
// playground/src/nl2ui-engine/composables/useCodeGenerator.tsconst generateVueCode = (dsl: UiDsl) => {
// 1. 逆向生成 Templateconst templateBody = dsl.components
.map(node => generateTemplateNode(node, 2))
.join('\n');
// 2. 智能分析依赖,生成 Scriptconst imports = analyzeImports(dsl.components);
// 3. 拼接成完整的 Vue 文件字符串return `<template>
<div class="generated-page">
${templateBody}
</div>
</template>
<script setup>
import { ${imports.join(', ')} } from 'vue-devui';
</script>`;
};
在我们的 Demo 右侧,专门做了一个“查看源码”的 Tab。点击它,你就能复制这段生成的代码,直接粘贴到你的项目里。这才是真正的提效。
五、 场景演示:MateChat 的“双面”能力
最后,我们看看这套系统在实际场景中的表现。我们设计了一个“左指令、右预览”的布局。
左边是大家熟悉的 MateChat 聊天窗口,它作为交互的入口。用户在这里输入自然语言,比如“帮我生成一个销售看板,要看总收入和活跃用户”。
MateChat 会显示“正在构建组件树...”,几秒钟后,右边的预览区就会实时渲染出一个包含数据卡片和图表的 Dashboard。
如果你输入“创建一个用户注册表单,包含用户名和密码”,右侧瞬间就会变成一个带有校验规则的 DevUI 表单。
这种“即问即答、即答即现”的体验,彻底改变了我们构建 UI 的方式。
六、 总结与展望
通过这个项目,我们验证了一个核心观点:受限的 DSL 反而是 AI 落地的最佳路径。
我们没有追求让 AI 直接写出完美的代码,而是利用 MateChat 做交互,利用 DSL 做约束,利用 DevUI 做渲染。这套组合拳打下来,既保证了系统的稳定性,又发挥了 AI 的灵活性。
未来,这套架构还有很大的想象空间。比如,我们可以把 DSL 喂给后端,直接生成数据库模型;或者结合语音识别,实现“动动嘴做软件”的科幻场景。
希望这个开源项目能给大家带来一点启发,也欢迎大家来 GitCode 提 PR,我们一起把这个 NL2UI 引擎打磨得更强大!
附官方链接: